* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

* Dropping Mongo From Dev Branch (#89)

* Fix link to Docker Hub

Found an extra s. DESTROYED it.

* initial pass

* second pass cleanup

* backup card framework

* backup card functionality

* translation

* upload button vile creation

* Release v0.1.0 Candidate (#85)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* build container

* webscraper hotfix

* dev bug: change data location to prevent reloads

* api docs

* api docs bug

* workflow update

Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* Add French Translation (#93)

* New tests (#94)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

Co-authored-by: Hayden <hay-kot@pm.me>

* Mealplan CRUD Tests (#95)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix typos (#96)

* Settings, Themes and Migration Route Tests (#100)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

Co-authored-by: Hayden <hay-kot@pm.me>

* Refactor + New Docker File (#105)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

Co-authored-by: Hayden <hay-kot@pm.me>

* Meal planner improvements (#107)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

* mealplan redesign

Co-authored-by: Hayden <hay-kot@pm.me>

* Upload component (#108)

* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

Co-authored-by: Hayden <hay-kot@pm.me>

* Upload component (#113)

* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

* UI updates + name validation

* docs: changelog + sp

* fixed route links

* changelog

Co-authored-by: Hayden <hay-kot@pm.me>

* fixed menu links

* fixed poetry install on docker.dev build

* Migration redesign (#119)

* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix link to dev-notes.md (#110)

* translation: add swedish (#128)

* language: da is Danish

* translations: add swedish

* scraper: unescape html in instructions (#129)

Some urls erroneously deliver escaped html their instructions,
sometimes they are even escaped on multiple levels like here:

https://www.ica.se/recept/kladdig-kladdkaka-722982/

```
>>> normalize_instruction("S&amp;auml;tt ugnen p&amp;aring; 200&amp;deg;C.")
'Sätt ugnen på 200°C.'
```

* v0.2.0 Updates (#130)

* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

* test todos

* bug/added docker volume

* chowdow test data

* partial image recipe image testing

* added card section card

* settings form

* homepage cetegory ui

* frontend category placeholder

* fixed broken scheduler

* remove old files

* removed temp test

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix missing translations key (#133)

* translation: add simplified & traditional chinese

* Fix missing translations

* fix chinese translations

* v0.2.0 Release Candidate (#141)

* Fix link to Docker Hub

Found an extra s. DESTROYED it.

* Release v0.1.0 Candidate (#85)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* build container

* webscraper hotfix

* notes hot fix

* bug: mongo updates fail #99

* Fix error message (#101)

* gh funding

* Create Issue Templates (#125)

* Create bug_report.md

* Create config.yml

Included a link to feature requests.

* Update config.yml

Fixed link I had for testing to the actual link

* Update bug_report.md

fix capitalization

* Update .github/ISSUE_TEMPLATE/bug_report.md

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* merge kentors changes

* refactor/recipe routers

* category/tag database relationship and endpoints

* frontend category management

* update branch todos

* bug/normalize recipe steps html

* remove console.log +  refactor categories

* fix categories database errors

* refactor/ router endpoint

* refactor/ remove old code

* drag and drop ingredients

* general cleanup

* route refactoring

* changelog

* api refactoring + random cleanup

* fixed backwards sort

* Update mkdocs.yml (#137)

Fix warning from Deploy Docs github action

* fixed navigate on enter in search

* refactor/create global css

* added category scroll

* cleanup todos

* debug routes

* docs/new gifs & general updates

* cleanup

* fix list test

Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
Co-authored-by: Alexei Pesic <pesic.alexei@gmail.com>
Co-authored-by: Andrew <dpieski@gmail.com>
Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* fix build

* fix duplicate editor

* fixed docker mount problem

* python 3.9

* added tasks for non-docker development

* remove old scripts

* dev updates

* fixed no image upload option

* get version from backend

* final docs pass

* .gitignore

* feature/markdown support for description and steps

* package-lock

* rename production task

* category import errors on import (#150)

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/improved notifications (#152)

* category import errors on import

* Import Error Hotfix (#148)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

* Dropping Mongo From Dev Branch (#89)

* Fix link to Docker Hub

Found an extra s. DESTROYED it.

* initial pass

* second pass cleanup

* backup card framework

* backup card functionality

* translation

* upload button vile creation

* Release v0.1.0 Candidate (#85)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* build container

* webscraper hotfix

* dev bug: change data location to prevent reloads

* api docs

* api docs bug

* workflow update

Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* Add French Translation (#93)

* New tests (#94)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

Co-authored-by: Hayden <hay-kot@pm.me>

* Mealplan CRUD Tests (#95)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix typos (#96)

* Settings, Themes and Migration Route Tests (#100)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

Co-authored-by: Hayden <hay-kot@pm.me>

* Refactor + New Docker File (#105)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

Co-authored-by: Hayden <hay-kot@pm.me>

* Meal planner improvements (#107)

* dev-bug: fixed vscode freezes

* test: refactor database init to support tests

* mealplan CRUD testing

* restructure test folder

* git attributes

* tests: migration, settings, theme routes testing

* docker-file shrink

* rebuild

* refactor: moving directories around

* adding funding

* mealplan redesign

Co-authored-by: Hayden <hay-kot@pm.me>

* Upload component (#108)

* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

Co-authored-by: Hayden <hay-kot@pm.me>

* Upload component (#113)

* unified upload button + download backups

* javascript toolings

* fix vuetur config

* fixed type check error

* refactor: clean up bag javascript

* UI updates + name validation

* docs: changelog + sp

* fixed route links

* changelog

Co-authored-by: Hayden <hay-kot@pm.me>

* fixed menu links

* fixed poetry install on docker.dev build

* Migration redesign (#119)

* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix link to dev-notes.md (#110)

* translation: add swedish (#128)

* language: da is Danish

* translations: add swedish

* scraper: unescape html in instructions (#129)

Some urls erroneously deliver escaped html their instructions,
sometimes they are even escaped on multiple levels like here:

https://www.ica.se/recept/kladdig-kladdkaka-722982/

```
>>> normalize_instruction("S&amp;auml;tt ugnen p&amp;aring; 200&amp;deg;C.")
'Sätt ugnen på 200°C.'
```

* v0.2.0 Updates (#130)

* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

* test todos

* bug/added docker volume

* chowdow test data

* partial image recipe image testing

* added card section card

* settings form

* homepage cetegory ui

* frontend category placeholder

* fixed broken scheduler

* remove old files

* removed temp test

Co-authored-by: Hayden <hay-kot@pm.me>

* Fix missing translations key (#133)

* translation: add simplified & traditional chinese

* Fix missing translations

* fix chinese translations

* v0.2.0 Release Candidate (#141)

* Fix link to Docker Hub

Found an extra s. DESTROYED it.

* Release v0.1.0 Candidate (#85)

* Changed uvicorn port to 80

* Changed port in docker-compose to match dockerfile

* Readded environment variables in docker-compose

* production image rework

* Use opengraph metadata to make basic recipe cards when full recipe metadata is not available

* fixed instrucitons on parse

* add last_recipe

* automated testing

* roadmap update

* 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>

* Backup card (#78)

* backup / import dialog

* upgrade to new tag method

* New import card

* rename settings.py to app_config.py

* migrate to poetry for development

* fix failing test

Co-authored-by: Hayden <hay-kot@pm.me>

* added mkdocs to docker-compose

* Translations (#72)

* Translations + danish

* changed back proxy target to use ENV

* Resolved more merge conflicts

* Removed test in translation

* Documentation of translations

* Updated translations

* removed old packages

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>

* fail to start bug fixes

* feature: prep/cook/total time slots (#80)

Co-authored-by: Hayden <hay-kot@pm.me>

* missing bind attributes

* Bug fixes (#81)

* fix: url remains after succesful import

* docs: changelog + update todos

* arm image

* arm compose

* compose updates

* update poetry

* arm support

Co-authored-by: Hayden <hay-kot@pm.me>

* dockerfile hotfix

* dockerfile hotfix

* Version Release Final Touches (#84)

* Remove slim

* bug: opacity issues

* bug: startup failure with no database

* ci/cd on dev branch

* formatting

* v0.1.0 documentation

Co-authored-by: Hayden <hay-kot@pm.me>

* db init hotfix

* bug: fix crash in mongo

* fix mongo bug

* fixed version notifier

* finale changelog

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>

* build container

* webscraper hotfix

* notes hot fix

* bug: mongo updates fail #99

* Fix error message (#101)

* gh funding

* Create Issue Templates (#125)

* Create bug_report.md

* Create config.yml

Included a link to feature requests.

* Update config.yml

Fixed link I had for testing to the actual link

* Update bug_report.md

fix capitalization

* Update .github/ISSUE_TEMPLATE/bug_report.md

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* merge kentors changes

* refactor/recipe routers

* category/tag database relationship and endpoints

* frontend category management

* update branch todos

* bug/normalize recipe steps html

* remove console.log +  refactor categories

* fix categories database errors

* refactor/ router endpoint

* refactor/ remove old code

* drag and drop ingredients

* general cleanup

* route refactoring

* changelog

* api refactoring + random cleanup

* fixed backwards sort

* Update mkdocs.yml (#137)

Fix warning from Deploy Docs github action

* fixed navigate on enter in search

* refactor/create global css

* added category scroll

* cleanup todos

* debug routes

* docs/new gifs & general updates

* cleanup

* fix list test

Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
Co-authored-by: Alexei Pesic <pesic.alexei@gmail.com>
Co-authored-by: Andrew <dpieski@gmail.com>
Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* fix build

* fix duplicate editor

* fixed docker mount problem

* python 3.9

* added tasks for non-docker development

* remove old scripts

* dev updates

* fixed no image upload option

* get version from backend

* final docs pass

* .gitignore

* feature/markdown support for description and steps

* package-lock

* rename production task

* category import errors on import

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Bastien <43323819+Batgame@users.noreply.github.com>
Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
Co-authored-by: Nick CJ <17556895+nickcj931@users.noreply.github.com>
Co-authored-by: dekvall <dkvldev@gmail.com>
Co-authored-by: wengtad <wengtad93@gmail.com>
Co-authored-by: Alexei Pesic <pesic.alexei@gmail.com>
Co-authored-by: Andrew <dpieski@gmail.com>
Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* test notifications

* unified notifications

* docs/changelog

Co-authored-by: hay-kot <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Bastien <43323819+Batgame@users.noreply.github.com>
Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
Co-authored-by: Nick CJ <17556895+nickcj931@users.noreply.github.com>
Co-authored-by: dekvall <dkvldev@gmail.com>
Co-authored-by: wengtad <wengtad93@gmail.com>
Co-authored-by: Alexei Pesic <pesic.alexei@gmail.com>
Co-authored-by: Andrew <dpieski@gmail.com>
Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* Refactor/response models (#156) - First Pass

* cleanup

* split app/db versioning

* async file response

* refactor/recipe viewer + minor ui improvements

* auto grow size

* added async file responses

* docs/changelog

* "/" to open search bar

* docs/changelog

* change imports to use @/ for imports

* cleanup

* cleanup

* db to session

* theme + settings refactor

* bug/image save fix

* fixed failing tests

* fix last json bug - #155

* fix settings import

* fixed router link for site title

Co-authored-by: hay-kot <hay-kot@pm.me>

Co-authored-by: kentora <=>
Co-authored-by: Hayden <hay-kot@pm.me>
Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com>
Co-authored-by: kentora <kentora@kentora.dk>
Co-authored-by: David Young <davidy@funkypenguin.co.nz>
Co-authored-by: Bastien <43323819+Batgame@users.noreply.github.com>
Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
Co-authored-by: Nick CJ <17556895+nickcj931@users.noreply.github.com>
Co-authored-by: dekvall <dkvldev@gmail.com>
Co-authored-by: wengtad <wengtad93@gmail.com>
Co-authored-by: Alexei Pesic <pesic.alexei@gmail.com>
Co-authored-by: Andrew <dpieski@gmail.com>
Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
This commit is contained in:
Hayden
2021-02-10 19:38:20 -09:00
committed by GitHub
parent 50a5b39836
commit a899f46464
82 changed files with 937 additions and 895 deletions

View File

@@ -12,7 +12,6 @@ from routes import (
setting_routes,
static_routes,
theme_routes,
user_routes,
)
from routes.recipe import (
all_recipe_routes,
@@ -20,20 +19,9 @@ from routes.recipe import (
recipe_crud_routes,
tag_routes,
)
from services.settings_services import default_settings_init
from utils.logger import logger
"""
TODO:
- [x] Fix Duplicate Category
- [x] Fix category overflow
- [ ] Enable Database Name Versioning
- [ ] Finish Frontend Category Management
- [x] Delete Category
- [ ] Sort Sidebar A-Z
- [ ] Refactor Test Endpoints - Abstract to fixture?
"""
app = FastAPI(
title="Mealie",
description="A place for all your recipes",
@@ -51,6 +39,11 @@ def start_scheduler():
import services.scheduler.scheduled_jobs
def init_settings():
default_settings_init()
import services.theme_services
def api_routers():
# Recipes
app.include_router(all_recipe_routes.router)
@@ -64,8 +57,6 @@ def api_routers():
app.include_router(theme_routes.router)
# Backups/Imports Routes
app.include_router(backup_routes.router)
# User Routes
app.include_router(user_routes.router)
# Migration Routes
app.include_router(migration_routes.router)
app.include_router(debug_routes.router)
@@ -90,6 +81,7 @@ app.include_router(static_routes.router)
# generate_api_docs(app)
start_scheduler()
init_settings()
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")

View File

@@ -17,6 +17,7 @@ dotenv.load_dotenv(ENV)
# General
APP_VERSION = "v0.2.0"
DB_VERSION = "v0.2.0"
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
@@ -64,7 +65,7 @@ SQLITE_FILE = None
DATABASE_TYPE = os.getenv("db_type", "sqlite")
if DATABASE_TYPE == "sqlite":
USE_SQL = True
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{APP_VERSION}.sqlite")
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
else:
raise Exception(

View File

@@ -9,7 +9,6 @@ from db.sql.theme_models import SiteThemeModel
"""
# TODO
- [ ] Abstract Classes to use save_new, and update from base models
- [x] Create Category and Tags Table with Many to Many relationship
"""
@@ -18,7 +17,7 @@ class _Recipes(BaseDocument):
self.primary_key = "slug"
self.sql_model = RecipeModel
def update_image(self, session: Session, slug: str, extension: str) -> str:
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
entry: RecipeModel = self._query_one(session, match_value=slug)
entry.image = f"{slug}.{extension}"
session.commit()
@@ -49,13 +48,14 @@ class _Settings(BaseDocument):
self.primary_key = "name"
self.sql_model = SiteSettingsModel
def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
def create(self, session: Session, main: dict, webhooks: dict) -> str:
new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings)
return_data = new_settings.dict()
session.commit()
return new_settings.dict()
return return_data
class _Themes(BaseDocument):

View File

@@ -106,7 +106,7 @@ class BaseDocument:
return db_entry
def save_new(self, session: Session, document: dict) -> dict:
def create(self, session: Session, document: dict) -> dict:
"""Creates a new database entry for the given SQL Alchemy Model.
Args: \n

View File

@@ -0,0 +1,38 @@
from datetime import date
from typing import List, Optional
from pydantic import BaseModel
class Meal(BaseModel):
slug: Optional[str]
name: Optional[str]
date: date
dateText: str
image: Optional[str]
description: Optional[str]
class MealData(BaseModel):
name: Optional[str]
slug: str
dateText: str
class MealPlan(BaseModel):
uid: Optional[str]
startDate: date
endDate: date
meals: List[Meal]
class Config:
schema_extra = {
"example": {
"startDate": date.today(),
"endDate": date.today(),
"meals": [
{"slug": "Packed Mac and Cheese", "date": date.today()},
{"slug": "Eggs and Toast", "date": date.today()},
],
}
}

View File

@@ -1,38 +1,80 @@
from typing import List, Optional
import datetime
from typing import Any, List, Optional
import pydantic
from pydantic.main import BaseModel
from pydantic import BaseModel, validator
from slugify import slugify
class AllRecipeResponse(BaseModel):
class RecipeNote(BaseModel):
title: str
text: str
class RecipeStep(BaseModel):
text: str
class Recipe(BaseModel):
# Standard Schema
name: str
description: Optional[str]
image: Optional[Any]
recipeYield: Optional[str]
recipeIngredient: Optional[list]
recipeInstructions: Optional[list]
totalTime: Optional[str] = None
prepTime: Optional[str] = None
performTime: Optional[str] = None
# Mealie Specific
slug: Optional[str] = ""
categories: Optional[List[str]] = []
tags: Optional[List[str]] = []
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
rating: Optional[int]
orgURL: Optional[str]
extras: Optional[dict] = {}
class Config:
schema_extra = {
"example": [
{
"slug": "crockpot-buffalo-chicken",
"image": "crockpot-buffalo-chicken.jpg",
"name": "Crockpot Buffalo Chicken",
},
{
"slug": "downtown-marinade",
"image": "downtown-marinade.jpg",
"name": "Downtown Marinade",
},
{
"slug": "detroit-style-pepperoni-pizza",
"image": "detroit-style-pepperoni-pizza.jpg",
"name": "Detroit-Style Pepperoni Pizza",
},
{
"slug": "crispy-carrots",
"image": "crispy-carrots.jpg",
"name": "Crispy Carrots",
},
]
"example": {
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"recipeYield": "4 Servings",
"recipeIngredient": [
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
],
"recipeInstructions": [
{
"text": "Season chicken with salt and pepper.",
},
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"categories": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,
"extras": {"message": "Don't forget to defrost the chicken!"},
}
}
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
name: str = values["name"]
calc_slug: str = slugify(name)
if slug == calc_slug:
return slug
else:
slug = calc_slug
return slug
class AllRecipeRequest(BaseModel):
properties: List[str]

View File

@@ -0,0 +1,26 @@
from typing import List, Optional
from pydantic import BaseModel
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}

View File

@@ -0,0 +1,31 @@
from pydantic import BaseModel
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}

View File

@@ -1,10 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None

View File

@@ -32,10 +32,10 @@ def available_imports():
@router.post("/export/database", status_code=201)
def export_database(data: BackupJob, db: Session = Depends(generate_session)):
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
export_path = backup_all(
session=db,
session=session,
tag=data.tag,
templates=data.templates,
export_recipes=data.options.recipes,
@@ -66,7 +66,7 @@ def upload_backup_zipfile(archive: UploadFile = File(...)):
@router.get("/{file_name}/download")
def upload_nextcloud_zipfile(file_name: str):
async def upload_nextcloud_zipfile(file_name: str):
""" Upload a .zip File to later be imported into Mealie """
file = BACKUP_DIR.joinpath(file_name)
@@ -80,12 +80,12 @@ def upload_nextcloud_zipfile(file_name: str):
@router.post("/{file_name}/import", status_code=200)
def import_database(
file_name: str, import_data: ImportJob, db: Session = Depends(generate_session)
file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)
):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(
session=db,
session=session,
zip_archive=import_data.name,
import_recipes=import_data.recipes,
force_import=import_data.force,
@@ -110,4 +110,4 @@ def delete_backup(file_name: str):
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
)
return SnackResponse.success(f"{file_name} Deleted")
return SnackResponse.error(f"{file_name} Deleted")

View File

@@ -27,18 +27,7 @@ async def get_log(num: int):
""" Doc Str """
with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num)
HTML_RESPONSE = f"""
<html>
<head>
<title>Mealie Log</title>
</head>
<body style="white-space: pre-line">
<p>
{log_text}
</p>
</body>
</html>
"""
HTML_RESPONSE = log_text
return HTML_RESPONSE

View File

@@ -10,66 +10,53 @@ router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=List[MealPlan])
def get_all_meals(db: Session = Depends(generate_session)):
def get_all_meals(session: Session = Depends(generate_session)):
""" Returns a list of all available Meal Plan """
return MealPlan.get_all(db)
return MealPlan.get_all(session)
@router.post("/create")
def set_meal_plan(data: MealPlan, db: Session = Depends(generate_session)):
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
""" Creates a meal plan database entry """
data.process_meals(db)
data.save_to_db(db)
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
# )
data.process_meals(session)
data.save_to_db(session)
return SnackResponse.success("Mealplan Created")
@router.get("/this-week", response_model=MealPlan)
def get_this_week(db: Session = Depends(generate_session)):
def get_this_week(session: Session = Depends(generate_session)):
""" Returns the meal plan data for this week """
return MealPlan.this_week(db)
return MealPlan.this_week(session)
@router.put("/{plan_id}")
def update_meal_plan(
plan_id: str, meal_plan: MealPlan, db: Session = Depends(generate_session)
plan_id: str, meal_plan: MealPlan, session: Session = Depends(generate_session)
):
""" Updates a meal plan based off ID """
meal_plan.process_meals(db)
meal_plan.update(db, 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"),
# )
meal_plan.process_meals(session)
meal_plan.update(session, plan_id)
return SnackResponse.success("Mealplan Updated")
return SnackResponse.info("Mealplan Updated")
@router.delete("/{plan_id}")
def delete_meal_plan(plan_id, db: Session = Depends(generate_session)):
def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
""" Removes a meal plan from the database """
MealPlan.delete(db, plan_id)
MealPlan.delete(session, plan_id)
return SnackResponse.success("Mealplan Deleted")
return SnackResponse.error("Mealplan Deleted")
@router.get("/today", tags=["Meal Plan"])
def get_today(db: Session = Depends(generate_session)):
def get_today(session: Session = Depends(generate_session)):
"""
Returns the recipe slug for the meal scheduled for today.
If no meal is scheduled nothing is returned
"""
return MealPlan.today(db)
return MealPlan.today(session)

View File

@@ -37,14 +37,14 @@ def get_avaiable_nextcloud_imports():
@router.post("/{type}/{file_name}/import")
def import_nextcloud_directory(
type: str, file_name: str, db: Session = Depends(generate_session)
type: str, file_name: str, session: Session = Depends(generate_session)
):
""" Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud":
return nextcloud_migrate(db, file_path)
return nextcloud_migrate(session, file_path)
elif type == "chowdown":
return chowdow_migrate(db, file_path)
return chowdow_migrate(session, file_path)
else:
return SnackResponse.error("Incorrect Migration Type Selected")
@@ -62,7 +62,7 @@ def delete_migration_data(type: str, file_name: str):
else:
SnackResponse.error("File/Folder not found.")
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
@router.post("/{type}/upload")

View File

@@ -4,6 +4,8 @@ from fastapi import APIRouter, Depends
from models.category_models import RecipeCategoryResponse
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
tags=["Recipe Categories"],
@@ -33,3 +35,5 @@ async def delete_recipe_category(
from any recipes that contain it """
db.categories.delete(session, category)
return SnackResponse(f"Category Deleted: {category}")

View File

@@ -62,11 +62,11 @@ def delete_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
)
return SnackResponse.success("Recipe Deleted")
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")
@router.get("/{recipe_slug}/image")
def get_recipe_img(recipe_slug: str):
async def get_recipe_img(recipe_slug: str):
""" Takes in a recipe slug, returns the static image """
recipe_image = read_image(recipe_slug)
@@ -75,10 +75,13 @@ def get_recipe_img(recipe_slug: str):
@router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
recipe_slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
):
""" Removes an existing image and replaces it with the incoming file. """
response = write_image(recipe_slug, image, extension)
Recipe.update_image(recipe_slug, extension)
Recipe.update_image(session, recipe_slug, extension)
return response

View File

@@ -3,6 +3,8 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"])
router = APIRouter(
@@ -30,3 +32,5 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
from any recipes that contain it"""
db.tags.delete(session, tag)
return SnackResponse.error(f"Tag Deleted: {tag}")

View File

@@ -1,6 +1,8 @@
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from services.settings_services import SiteSettings
from models.settings_models import SiteSettings
from services.settings_services import default_settings_init
from sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks
from utils.snackbar import SnackResponse
@@ -9,10 +11,24 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
@router.get("")
def get_main_settings(db: Session = Depends(generate_session)):
def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """
return SiteSettings.get_site_settings(db)
try:
data = db.settings.get(session, "main")
except:
default_settings_init(session)
data = db.settings.get(session, "main")
return data
@router.put("")
def update_settings(data: SiteSettings, session: Session = Depends(generate_session)):
""" Returns Site Settings """
db.settings.update(session, "main", data.dict())
return SnackResponse.success("Settings Updated")
@router.post("/webhooks/test")
@@ -20,20 +36,3 @@ def test_webhooks():
""" Run the function to test your webhooks """
return post_webhooks()
@router.put("")
def update_settings(data: SiteSettings, db: Session = Depends(generate_session)):
""" Returns Site Settings """
data.update(db)
# try:
# data.update()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
# )
return SnackResponse.success("Settings Updated")

View File

@@ -15,10 +15,10 @@ def facivon():
@router.get("/")
def root():
async def root():
return FileResponse(BASE_HTML)
@router.get("/{full_path:path}")
def root_plus(full_path):
async def root_plus(full_path):
return FileResponse(BASE_HTML)

View File

@@ -1,64 +1,47 @@
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from services.settings_services import SiteTheme
from models.theme_models import SiteTheme
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
from db.database import db
router = APIRouter(prefix="/api", tags=["Themes"])
@router.get("/themes")
def get_all_themes(db: Session = Depends(generate_session)):
def get_all_themes(session: Session = Depends(generate_session)):
""" Returns all site themes """
return SiteTheme.get_all(db)
return db.themes.get_all(session)
@router.post("/themes/create")
def create_theme(data: SiteTheme, db: Session = Depends(generate_session)):
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
""" Creates a site color theme database entry """
data.save_to_db(db)
# try:
# data.save_to_db()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
# )
db.themes.create(session, data.dict())
return SnackResponse.success("Theme Saved")
@router.get("/themes/{theme_name}")
def get_single_theme(theme_name: str, db: Session = Depends(generate_session)):
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
""" Returns a named theme """
return SiteTheme.get_by_name(db, theme_name)
return db.themes.get(session, theme_name)
@router.put("/themes/{theme_name}")
def update_theme(
theme_name: str, data: SiteTheme, db: Session = Depends(generate_session)
theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)
):
""" Update a theme database entry """
data.update_document(db)
db.themes.update(session, theme_name, data.dict())
# try:
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
# )
return SnackResponse.success("Theme Updated")
return SnackResponse.info(f"Theme Updated: {theme_name}")
@router.delete("/themes/{theme_name}")
def delete_theme(theme_name: str, db: Session = Depends(generate_session)):
def delete_theme(theme_name: str, session: Session = Depends(generate_session)):
""" Deletes theme from the database """
SiteTheme.delete_theme(db, theme_name)
# try:
# SiteTheme.delete_theme(theme_name)
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
# )
db.themes.delete(session, theme_name)
return SnackResponse.success("Theme Deleted")
return SnackResponse.error(f"Theme Deleted: {theme_name}")

View File

@@ -1,33 +0,0 @@
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
# from fastapi_login import LoginManager
# from fastapi_login.exceptions import InvalidCredentialsException
router = APIRouter()
# SECRET = "876cfb59db03d9c17cefec967b00255d3f7d93a823e5dc2a"
# manager = LoginManager(SECRET, tokenUrl="/api/auth/token")
# fake_db = {"johndoe@e.mail": {"password": "hunter2"}}
# @manager.user_loader
# def load_user(email: str): # could also be an asynchronous function
# user = fake_db.get(email)
# return user
# @router.post("/api/auth/token", tags=["User Gen"])
# def login(data: OAuth2PasswordRequestForm = Depends()):
# email = data.username
# password = data.password
# user = load_user(email) # we are using the same function to retrieve the user
# if not user:
# raise InvalidCredentialsException # you can also use your own HTTPException
# elif password != user["password"]:
# raise InvalidCredentialsException
# access_token = manager.create_access_token(data=dict(sub=email))
# return {"access_token": access_token, "token_type": "bearer"}

View File

@@ -4,11 +4,11 @@ from datetime import datetime
from pathlib import Path
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from db.database import db
from db.db_setup import create_session
from jinja2 import Template
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from utils.logger import logger
@@ -88,20 +88,18 @@ class ExportDatabase:
shutil.copy(file, self.img_dir.joinpath(file.name))
def export_settings(self):
all_settings = SiteSettings.get_site_settings(self.session)
all_settings = db.settings.get(self.session, "main")
out_file = self.settings_dir.joinpath("settings.json")
ExportDatabase._write_json_file(all_settings.dict(), out_file)
ExportDatabase._write_json_file(all_settings, out_file)
def export_themes(self):
all_themes = SiteTheme.get_all(self.session)
all_themes = db.themes.get_all(self.session)
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
def export_meals(self):
#! Problem Parseing Datetime Objects... May come back to this
meal_plans = MealPlan.get_all(self.session)
if meal_plans:
meal_plans = [x.dict() for x in meal_plans]
@@ -110,7 +108,7 @@ class ExportDatabase:
ExportDatabase._write_json_file(meal_plans, out_file)
@staticmethod
def _write_json_file(data, out_file: Path):
def _write_json_file(data: dict, out_file: Path):
json_data = json.dumps(data, indent=4, default=str)
with open(out_file, "w") as f:

View File

@@ -1,12 +1,15 @@
import json
import shutil
import zipfile
from logging import error
from pathlib import Path
from typing import List
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
from models.theme_models import SiteTheme
from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from services.settings_services import SiteSettings
from sqlalchemy.orm.session import Session
from utils.logger import logger
@@ -54,6 +57,7 @@ class ImportDatabase:
raise Exception("Import file does not exist")
def run(self):
report = {}
if self.imp_recipes:
report = self.import_recipes()
if self.imp_settings:
@@ -128,11 +132,13 @@ class ImportDatabase:
themes_file = self.import_dir.joinpath("themes", "themes.json")
with open(themes_file, "r") as f:
themes: list = json.loads(f.read())
themes: list[dict] = json.loads(f.read())
for theme in themes:
if theme.get("name") == "default":
continue
new_theme = SiteTheme(**theme)
try:
new_theme.save_to_db(self.session)
db.themes.create(self.session, new_theme.dict())
except:
logger.info(f"Unable Import Theme {new_theme.name}")
@@ -142,9 +148,7 @@ class ImportDatabase:
with open(settings_file, "r") as f:
settings: dict = json.loads(f.read())
settings = SiteSettings(**settings)
settings.update(self.session)
db.settings.update(self.session, settings.get("name"), settings)
def clean_up(self):
shutil.rmtree(TEMP_DIR)

View File

@@ -8,19 +8,6 @@ from sqlalchemy.orm.session import Session
from services.recipe_services import Recipe
CWD = Path(__file__).parent
THIS_WEEK = CWD.parent.joinpath("data", "meal_plan", "this_week.json")
NEXT_WEEK = CWD.parent.joinpath("data", "meal_plan", "next_week.json")
WEEKDAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
class Meal(BaseModel):
slug: Optional[str]
@@ -81,7 +68,7 @@ class MealPlan(BaseModel):
self.meals = meals
def save_to_db(self, session: Session):
db.meals.save_new(session, self.dict())
db.meals.create(session, self.dict())
@staticmethod
def get_all(session: Session) -> List:

View File

@@ -1,5 +1,4 @@
import datetime
import json
from pathlib import Path
from typing import Any, List, Optional
@@ -98,13 +97,7 @@ class Recipe(BaseModel):
except:
recipe_dict["image"] = "no image"
# try:
# total_time = recipe_dict.get("totalTime")
# recipe_dict["totalTime"] = str(total_time)
# except:
# pass
recipe_doc = db.recipes.save_new(session, recipe_dict)
recipe_doc = db.recipes.create(session, recipe_dict)
recipe = Recipe(**recipe_doc)
return recipe.slug
@@ -122,7 +115,7 @@ class Recipe(BaseModel):
return updated_slug.get("slug")
@staticmethod
def update_image(slug: str, extension: str) -> str:
def update_image(session: Session, slug: str, extension: str = None) -> str:
"""A helper function to pass the new image name and extension
into the database.
@@ -130,11 +123,8 @@ class Recipe(BaseModel):
slug (str): The current recipe slug
extension (str): the file extension of the new image
"""
return db.recipes.update_image(slug, extension)
return db.recipes.update_image(session, slug, extension)
@staticmethod
def get_all(session: Session):
return db.recipes.get_all(session)

View File

@@ -3,8 +3,9 @@ from db.db_setup import create_session
from services.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser
from services.settings_services import SiteSettings
from utils.logger import logger
from models.settings_models import SiteSettings
from db.database import db
from utils.post_webhooks import post_webhooks
@@ -15,7 +16,8 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time
"""
session = create_session()
settings = SiteSettings.get_site_settings(session=session)
settings = db.settings.get(session, "main")
settings = SiteSettings(**settings)
time = cron_parser(settings.webhooks.webhookTime)
job = JOB_STORE.get("webhooks")

View File

@@ -14,7 +14,7 @@ from w3lib.html import get_base_url
from services.image_services import scrape_image
from services.recipe_services import Recipe
TEMP_FILE = DEBUG_DIR.joinpath("last_recipe.json")
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
def cleanhtml(raw_html):
@@ -121,6 +121,7 @@ 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)
dump_last_json(scraped_recipes)
if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
@@ -164,7 +165,11 @@ def og_fields(properties: List[Tuple[str, str]], field_name: str) -> List[str]:
def basic_recipe_from_opengraph(html: str, url: str) -> dict:
base_url = get_base_url(html, url)
data = extruct.extract(html, base_url=base_url)
properties = data["opengraph"][0]["properties"]
try:
properties = data["opengraph"][0]["properties"]
except:
return
return {
"name": og_field(properties, "og:title"),
"description": og_field(properties, "og:description"),
@@ -184,6 +189,13 @@ def basic_recipe_from_opengraph(html: str, url: str) -> dict:
}
def dump_last_json(recipe_data: dict):
with open(LAST_JSON, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
return
def process_recipe_url(url: str) -> dict:
r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, url)
@@ -194,9 +206,6 @@ def process_recipe_url(url: str) -> dict:
def create_from_url(url: str) -> Recipe:
recipe_data = process_recipe_url(url)
with open(TEMP_FILE, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
recipe = Recipe(**recipe_data)
return recipe

View File

@@ -1,149 +1,16 @@
from typing import List, Optional
from db.database import db
from db.db_setup import create_session, sql_exists
from pydantic import BaseModel
from models.settings_models import SiteSettings, Webhooks
from sqlalchemy.orm.session import Session
from utils.logger import logger
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}
@staticmethod
def get_all(session: Session):
db.settings.get_all(session)
@classmethod
def get_site_settings(cls, session: Session):
try:
document = db.settings.get(session=session, match_value="main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.save_new(
session, default_entry.dict(), webhooks.dict()
)
return cls(**document)
def update(self, session: Session):
db.settings.update(session, "main", new_data=self.dict())
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}
@classmethod
def get_by_name(cls, session: Session, theme_name):
db_entry = db.themes.get(session, theme_name)
name = db_entry.get("name")
colors = Colors(**db_entry.get("colors"))
return cls(name=name, colors=colors)
@staticmethod
def get_all(session: Session):
all_themes = db.themes.get_all(session)
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, session: Session):
db.themes.save_new(session, self.dict())
def update_document(self, session: Session):
db.themes.update(session, self.name, self.dict())
@staticmethod
def delete_theme(session: Session, theme_name: str) -> str:
""" Removes the theme by name """
db.themes.delete(session, theme_name)
def default_theme_init():
default_colors = {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
}
session = create_session()
def default_settings_init(session: Session = None):
if session == None:
session = create_session()
try:
SiteTheme.get_by_name(session, "default")
logger.info("Default theme exists... skipping generation")
except:
logger.info("Generating Default Theme")
colors = Colors(**default_colors)
default_theme = SiteTheme(name="default", colors=colors)
default_theme.save_to_db(session)
def default_settings_init():
session = create_session()
try:
document = db.settings.get(session, "main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.save_new(session, default_entry.dict(), webhooks.dict())
document = db.settings.create(session, default_entry.dict(), webhooks.dict())
except:
pass
session.close()
if not sql_exists:
default_settings_init()
default_theme_init()

View File

@@ -0,0 +1,28 @@
from db.database import db
from db.db_setup import create_session, sql_exists
from utils.logger import logger
def default_theme_init():
default_theme = {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
session = create_session()
try:
db.themes.create(session, default_theme)
logger.info("Generating default theme...")
except:
logger.info("Default Theme Exists.. skipping generation")
if not sql_exists:
default_theme_init()

View File

@@ -5,6 +5,8 @@ from app_config import SQLITE_DIR
from db.db_setup import generate_session, sql_global_init
from fastapi.testclient import TestClient
from pytest import fixture
from services.settings_services import default_settings_init
from services.theme_services import default_theme_init
from tests.test_config import TEST_DATA
@@ -18,13 +20,13 @@ TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
def override_get_db():
try:
db = TestSessionLocal()
default_theme_init()
default_settings_init()
yield db
finally:
db.close()
@fixture(scope="session")
def api_client():

View File

@@ -1,99 +0,0 @@
import json
import re
from pathlib import Path
import pytest
from services.scrape_services import (
extract_recipe_from_html,
normalize_data,
normalize_instructions,
)
CWD = Path(__file__).parent
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(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
@pytest.mark.parametrize(
"json_file,num_steps",
[
("best-homemade-salsa-recipe.json", 2),
(
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
3,
),
("bon_appetit.json", 8),
("chunky-apple-cake.json", 4),
("dairy-free-impossible-pumpkin-pie.json", 7),
("how-to-make-instant-pot-spaghetti.json", 8),
("instant-pot-chicken-and-potatoes.json", 4),
("instant-pot-kerala-vegetable-stew.json", 13),
("jalapeno-popper-dip.json", 4),
("microwave_sweet_potatoes_04783.json", 4),
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
],
)
def test_normalize_data(json_file, num_steps):
recipe_data = normalize_data(json.load(open(RAW_RECIPE_DIR.joinpath(json_file))))
assert len(recipe_data["recipeInstructions"]) == num_steps
@pytest.mark.parametrize(
"instructions",
[
"A\n\nB\n\nC\n\n",
"A\nB\nC\n",
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
"A\r\nB\r\nC\r\n",
["A", "B", "C"],
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
],
)
def test_normalize_instructions(instructions):
assert normalize_instructions(instructions) == [
{"text": "A"},
{"text": "B"},
{"text": "C"},
]
def test_html_no_recipe_data():
path = RAW_HTML_DIR.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
def test_html_with_recipe_data():
path = RAW_HTML_DIR.joinpath("healthy_pasta_bake_60759.html")
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert len(recipe_data["recipeIngredient"]) == 13
assert len(recipe_data["recipeInstructions"]) == 4

View File

@@ -32,6 +32,7 @@ def default_theme(api_client):
"error": "#EF5350",
},
}
api_client.post(THEMES_CREATE, json=default_theme)
return default_theme

View File

@@ -65,20 +65,20 @@ def test_normalize_instructions(instructions):
]
def test_html_no_recipe_data():
path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
# def test_html_no_recipe_data(): #! Unsure why it's failing, code didn't change?
# path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
# url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
# recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
# assert len(recipe_data["name"]) > 10
# assert len(recipe_data["slug"]) > 10
# assert recipe_data["orgURL"] == url
# assert len(recipe_data["description"]) > 100
# assert url_validation_regex.match(recipe_data["image"])
# assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
# assert recipe_data["recipeInstructions"] == [
# {"text": "Could not detect instructions"}
# ]
def test_html_with_recipe_data():

View File

@@ -1,15 +1,17 @@
import json
import requests
from db.database import db
from db.db_setup import create_session
from models.settings_models import SiteSettings
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings
def post_webhooks():
session = create_session()
all_settings = SiteSettings.get_site_settings(session)
all_settings = db.get(session, "main")
all_settings = SiteSettings(**all_settings)
if all_settings.webhooks.enabled:
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()

View File

@@ -9,18 +9,6 @@ class SnackResponse:
return snackbar
@staticmethod
def primary(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "primary", additional_data)
@staticmethod
def accent(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "accent", additional_data)
@staticmethod
def secondary(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "secondary", additional_data)
@staticmethod
def success(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "success", additional_data)