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

* version update

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:41:48 -09:00 committed by GitHub
parent 8221c36a89
commit c430c8da33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 938 additions and 896 deletions

View File

@ -1,5 +1,20 @@
# Release Notes # Release Notes
## v0.3.0 - Draft!
### Features and Improvements
- Open search with `/` hotkey!
- Unified and improved snackbar notifications
- Recipe Viewer
- Categories, Tags, and Notes will not be displayed below the steps on smaller screens
- Recipe Editor
- Text areas now auto grow to fit content
- Description, Steps, and Notes support Markdown! This includes inline html in Markdown.
### Development / Misc
- Added async file response for images, downloading files.
- Breakup recipe view component
## v0.2.0 - Now with Test! ## v0.2.0 - Now with Test!
This is, what I think, is a big release! Tons of new features and some great quality of life improvements with some additional features. You may find that I made promises to include some fixes/features in v0.2.0. The short of is I greatly underestimated the work needed to refactor the database to a usable state and integrate categories in a way that is useful for users. This shouldn't be taken as a sign that I'm dropping those feature requests or ignoring them. I felt it was better to push a release in the current state rather than drag on development to try and fulfil all of the promises I made. This is, what I think, is a big release! Tons of new features and some great quality of life improvements with some additional features. You may find that I made promises to include some fixes/features in v0.2.0. The short of is I greatly underestimated the work needed to refactor the database to a usable state and integrate categories in a way that is useful for users. This shouldn't be taken as a sign that I'm dropping those feature requests or ignoring them. I felt it was better to push a release in the current state rather than drag on development to try and fulfil all of the promises I made.

View File

@ -1385,6 +1385,11 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true "dev": true
}, },
"@smartweb/vue-flash-message": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@smartweb/vue-flash-message/-/vue-flash-message-0.6.10.tgz",
"integrity": "sha512-ceDUUzXI6FDscev36kZQvc2BO+MayOt6uJ2HSh9zoOkfa0PVIhmaoB56InlTTsK7MmlSIvPJpRB+Habdx3MtNw=="
},
"@soda/friendly-errors-webpack-plugin": { "@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@ -2011,16 +2016,6 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true "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": { "cacache": {
"version": "13.0.1", "version": "13.0.1",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
@ -2047,53 +2042,6 @@
"unique-filename": "^1.1.1" "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
},
"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"
}
},
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2110,16 +2058,6 @@
"minipass": "^3.1.1" "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": { "terser-webpack-plugin": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@ -2136,18 +2074,6 @@
"terser": "^4.6.12", "terser": "^4.6.12",
"webpack-sources": "^1.4.3" "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"
}
} }
} }
}, },
@ -11935,6 +11861,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": { "vue-router": {
"version": "3.4.9", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@adapttive/vue-markdown": "^3.0.3", "@adapttive/vue-markdown": "^3.0.3",
"@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.8.2", "core-js": "^3.8.2",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",

View File

@ -1,16 +1,22 @@
<template> <template>
<v-app> <v-app>
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none"> <v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
<v-btn @click="$router.push('/')" icon> <router-link to="/">
<v-btn icon>
<v-icon size="40"> mdi-silverware-variant </v-icon> <v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn> </v-btn>
</router-link>
<div btn class="pl-2"> <div btn class="pl-2">
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title> <v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
>Mealie
</v-toolbar-title>
</div> </div>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-expand-x-transition> <v-expand-x-transition>
<SearchBar <SearchBar
ref="mainSearchBar"
class="mt-7" class="mt-7"
v-if="search" v-if="search"
:show-results="true" :show-results="true"
@ -29,6 +35,7 @@
<SnackBar /> <SnackBar />
<router-view></router-view> <router-view></router-view>
</v-container> </v-container>
<FlashMessage :position="'right bottom'"></FlashMessage>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
@ -54,6 +61,13 @@ export default {
this.search = false; this.search = false;
}, },
}, },
created() {
window.addEventListener("keyup", e => {
if (e.key == "/") {
this.search = !this.search;
}
});
},
mounted() { mounted() {
this.$store.dispatch("initTheme"); this.$store.dispatch("initTheme");
@ -94,5 +108,34 @@ export default {
</script> </script>
<style> <style>
.notify-info-color {
border: 1px, solid, var(--v-info-base) !important;
border-left: 3px, solid, var(--v-info-base) !important;
background-color: var(--v-info-base) !important;
}
.notify-warning-color {
border: 1px, solid, var(--v-warning-base) !important;
border-left: 3px, solid, var(--v-warning-base) !important;
background-color: var(--v-warning-base) !important;
}
.notify-error-color {
border: 1px, solid, var(--v-error-base) !important;
border-left: 3px, solid, var(--v-error-base) !important;
background-color: var(--v-error-base) !important;
}
.notify-success-color {
border: 1px, solid, var(--v-success-base) !important;
border-left: 3px, solid, var(--v-success-base) !important;
background-color: var(--v-success-base) !important;
}
.notify-base {
color: white !important;
margin-right: 60px;
margin-bottom: -5px;
opacity: 0.9 !important;
}
</style> </style>

View File

@ -8,7 +8,7 @@ import myUtils from "./api/upload";
import category from "./api/category"; import category from "./api/category";
import meta from "./api/meta"; import meta from "./api/meta";
// import api from "../api"; // import api from "@/api";
export default { export default {
recipes: recipe, recipes: recipe,

View File

@ -1,23 +1,20 @@
const baseURL = "/api/"; const baseURL = "/api/";
import axios from "axios"; import axios from "axios";
import store from "../store/store"; import utils from "@/utils";
// look for data.snackbar in response
function processResponse(response) { function processResponse(response) {
try { try {
store.commit("setSnackBar", { utils.notify.show(response.data.snackbar.text, response.data.snackbar.type);
text: response.data.snackbar.text,
type: response.data.snackbar.type,
});
} catch (err) { } catch (err) {
return; return;
} }
return; return;
} }
const apiReq = { const apiReq = {
post: async function (url, data) { post: async function(url, data) {
let response = await axios.post(url, data).catch(function (error) { let response = await axios.post(url, data).catch(function(error) {
if (error.response) { if (error.response) {
processResponse(error.response); processResponse(error.response);
return error.response; return error.response;
@ -27,8 +24,19 @@ const apiReq = {
return response; return response;
}, },
put: async function (url, data) { put: async function(url, data) {
let response = await axios.put(url, data).catch(function (error) { let response = await axios.put(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
get: async function(url, data) {
let response = await axios.get(url, data).catch(function(error) {
if (error.response) { if (error.response) {
processResponse(error.response); processResponse(error.response);
return response; return response;
@ -38,19 +46,8 @@ const apiReq = {
return response; return response;
}, },
get: async function (url, data) { delete: async function(url, data) {
let response = await axios.get(url, data).catch(function (error) { let response = await axios.delete(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
// processResponse(response);
return response;
},
delete: async function (url, data) {
let response = await axios.delete(url, data).catch(function (error) {
if (error.response) { if (error.response) {
processResponse(error.response); processResponse(error.response);
return response; return response;

View File

@ -5,10 +5,10 @@ const prefix = baseURL + "themes";
const settingsURLs = { const settingsURLs = {
allThemes: `${baseURL}themes`, allThemes: `${baseURL}themes`,
specificTheme: (themeName) => `${prefix}/${themeName}`, specificTheme: themeName => `${prefix}/${themeName}`,
createTheme: `${prefix}/create`, createTheme: `${prefix}/create`,
updateTheme: (themeName) => `${prefix}/${themeName}`, updateTheme: themeName => `${prefix}/${themeName}`,
deleteTheme: (themeName) => `${prefix}/${themeName}`, deleteTheme: themeName => `${prefix}/${themeName}`,
}; };
export default { export default {
@ -33,6 +33,7 @@ export default {
colors: colors, colors: colors,
}; };
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body); let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
console.log(response.data);
return response.data; return response.data;
}, },

View File

@ -1,7 +1,7 @@
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
export default { export default {
// import api from "../api"; // import api from "@/api";
async uploadFile(url, fileObject) { async uploadFile(url, fileObject) {
let response = await apiReq.post(url, fileObject, { let response = await apiReq.post(url, fileObject, {
headers: { headers: {

View File

@ -26,7 +26,7 @@
</template> </template>
<script> <script>
import utils from "../../utils"; import utils from "@/utils";
import SearchDialog from "../UI/SearchDialog"; import SearchDialog from "../UI/SearchDialog";
export default { export default {
components: { components: {

View File

@ -20,8 +20,8 @@
</template> </template>
<script> <script>
import api from "../../api"; import api from "@/api";
import utils from "../../utils"; import utils from "@/utils";
import MealPlanCard from "./MealPlanCard"; import MealPlanCard from "./MealPlanCard";
export default { export default {
components: { components: {

View File

@ -85,8 +85,8 @@
</template> </template>
<script> <script>
import api from "../../api"; import api from "@/api";
import utils from "../../utils"; import utils from "@/utils";
import MealPlanCard from "./MealPlanCard"; import MealPlanCard from "./MealPlanCard";
export default { export default {
components: { components: {

View File

@ -41,21 +41,23 @@
> >
</v-text-field> </v-text-field>
<v-textarea <v-textarea
height="100" auto-grow
min-height="100"
:label="$t('recipe.description')" :label="$t('recipe.description')"
v-model="value.description" v-model="value.description"
> >
</v-textarea> </v-textarea>
<div class="my-2"></div> <div class="my-2"></div>
<v-row dense disabled> <v-row dense disabled>
<v-col sm="5"> <v-col sm="4">
<v-text-field <v-text-field
:label="$t('recipe.servings')" :label="$t('recipe.servings')"
v-model="value.recipeYield" v-model="value.recipeYield"
class="rounded-sm"
> >
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col></v-col> <v-spacer></v-spacer>
<v-rating <v-rating
class="mr-2 align-end" class="mr-2 align-end"
color="secondary darken-1" color="secondary darken-1"
@ -186,6 +188,7 @@
</v-row> </v-row>
<v-textarea <v-textarea
auto-grow
:label="$t('recipe.note')" :label="$t('recipe.note')"
v-model="value.notes[index]['text']" v-model="value.notes[index]['text']"
> >
@ -218,17 +221,18 @@
elevation="0" elevation="0"
@click="removeStep(index)" @click="removeStep(index)"
> >
<v-icon color="error">mdi-delete</v-icon> </v-btn <v-icon color="error">mdi-delete</v-icon>
>{{ </v-btn>
$t("recipe.step-index", { step: index + 1 }) {{ $t("recipe.step-index", { step: index + 1 }) }}
}}</v-card-title </v-card-title>
>
<v-card-text> <v-card-text>
<v-textarea <v-textarea
auto-grow
dense dense
v-model="value.recipeInstructions[index]['text']" v-model="value.recipeInstructions[index]['text']"
:key="generateKey('instructions', index)" :key="generateKey('instructions', index)"
></v-textarea> >
</v-textarea>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-hover> </v-hover>
@ -250,8 +254,8 @@
<script> <script>
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import api from "../../../api"; import api from "@/api";
import utils from "../../../utils"; import utils from "@/utils";
import BulkAdd from "./BulkAdd"; import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor"; import ExtrasEditor from "./ExtrasEditor";
export default { export default {

View File

@ -161,7 +161,7 @@
</template> </template>
<script> <script>
import utils from "../../utils"; import utils from "@/utils";
export default { export default {
props: { props: {

View File

@ -1,193 +0,0 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
<vue-markdown :source="description"> </vue-markdown>
<div class="my-2"></div>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
<div v-if="categories[0]">
<h2 class="mt-4">{{ $t("recipe.categories") }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in categories"
:key="category"
>
{{ category }}
</v-chip>
</div>
<div v-if="tags[0]">
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="tag in tags"
:key="tag"
>
{{ tag }}
</v-chip>
</div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
{{ note.text }}
</v-card-text>
</v-card>
</v-col>
<v-divider class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in instructions"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown>
{{ step.text }}
</vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</v-col>
</v-row>
<v-row>
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "../../utils";
export default {
components: {
VueMarkdown,
},
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
</div>
</template>
<script>
import utils from "@/utils";
export default {
props: {
ingredients: Array,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,36 @@
<template>
<div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</v-card>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
notes: Array,
},
components: {
VueMarkdown,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,26 @@
<template>
<div v-if="items[0]">
<h2 class="mt-4">{{ title }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in items"
:key="category"
>
{{ category }}
</v-chip>
</div>
</template>
<script>
export default {
props: {
items: Array,
title: String,
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in steps"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
steps: Array,
},
components: {
VueMarkdown,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,130 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
<vue-markdown :source="description"> </vue-markdown>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients :ingredients="ingredients" />
<div v-if="medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
</v-col>
<v-divider
v-if="medium"
class="my-divider"
:vertical="true"
></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Steps :steps="instructions" />
</v-col>
</v-row>
<div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
import RecipeChips from "./RecipeChips";
import Steps from "./Steps";
import Notes from "./Notes";
import Ingredients from "./Ingredients";
export default {
components: {
VueMarkdown,
RecipeChips,
Steps,
Notes,
Ingredients,
},
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
data() {
return {
disabledSteps: [],
};
},
computed: {
medium() {
return this.$vuetify.breakpoint.mdAndUp;
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View File

@ -38,8 +38,8 @@
<script> <script>
import ImportDialog from "./ImportDialog"; import ImportDialog from "./ImportDialog";
import api from "../../../api"; import api from "@/api";
import utils from "../../../utils"; import utils from "@/utils";
export default { export default {
props: { props: {
backups: Array, backups: Array,

View File

@ -38,8 +38,8 @@
<script> <script>
import ImportDialog from "./ImportDialog"; import ImportDialog from "./ImportDialog";
import api from "../../../api"; import api from "@/api";
import utils from "../../../utils"; import utils from "@/utils";
export default { export default {
props: { props: {
backups: Array, backups: Array,

View File

@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import api from "../../../api"; import api from "@/api";
export default { export default {
data() { data() {
return { return {
@ -84,7 +84,7 @@ export default {
methods: { methods: {
async getAvailableBackups() { async getAvailableBackups() {
let response = await api.backups.requestAvailable(); let response = await api.backups.requestAvailable();
response.templates.forEach((element) => { response.templates.forEach(element => {
this.availableTemplates.push(element); this.availableTemplates.push(element);
}); });
}, },
@ -101,7 +101,6 @@ export default {
templates: this.selectedTemplates, templates: this.selectedTemplates,
}; };
await api.backups.create(data); await api.backups.create(data);
this.loading = false; this.loading = false;

View File

@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import api from "../../../api"; import api from "@/api";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert"; import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import UploadBtn from "../../UI/UploadBtn"; import UploadBtn from "../../UI/UploadBtn";
import AvailableBackupCard from "./AvailableBackupCard"; import AvailableBackupCard from "./AvailableBackupCard";

View File

@ -126,7 +126,7 @@
</template> </template>
<script> <script>
import api from "../../../api"; import api from "@/api";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
export default { export default {

View File

@ -56,8 +56,8 @@
<script> <script>
import UploadBtn from "../../UI/UploadBtn"; import UploadBtn from "../../UI/UploadBtn";
import utils from "../../../utils"; import utils from "@/utils";
import api from "../../../api"; import api from "@/api";
export default { export default {
props: { props: {
folder: String, folder: String,

View File

@ -41,7 +41,7 @@
<script> <script>
import MigrationCard from "./MigrationCard"; import MigrationCard from "./MigrationCard";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert"; import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import api from "../../../api"; import api from "@/api";
export default { export default {
components: { components: {
MigrationCard, MigrationCard,
@ -78,7 +78,7 @@ export default {
}, },
async getAvailableMigrations() { async getAvailableMigrations() {
let response = await api.migrations.getMigrations(); let response = await api.migrations.getMigrations();
response.forEach((element) => { response.forEach(element => {
if (element.type === "nextcloud") { if (element.type === "nextcloud") {
this.migrations.nextcloud.availableImports = element.files; this.migrations.nextcloud.availableImports = element.files;
} else if (element.type === "chowdown") { } else if (element.type === "chowdown") {

View File

@ -53,7 +53,7 @@
return-object return-object
v-model="selectedTheme" v-model="selectedTheme"
@change="themeSelected" @change="themeSelected"
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]" :rules="[v => !!v || $t('settings.theme.theme-is-required')]"
required required
> >
</v-select> </v-select>
@ -136,7 +136,7 @@
</template> </template>
<script> <script>
import api from "../../../api"; import api from "@/api";
import ColorPickerDialog from "./ColorPickerDialog"; import ColorPickerDialog from "./ColorPickerDialog";
import NewThemeDialog from "./NewThemeDialog"; import NewThemeDialog from "./NewThemeDialog";
import Confirmation from "../../UI/Confirmation"; import Confirmation from "../../UI/Confirmation";
@ -186,7 +186,7 @@ export default {
//Change to default if deleting current theme. //Change to default if deleting current theme.
if ( if (
!this.availableThemes.some( !this.availableThemes.some(
(theme) => theme.name === this.selectedTheme.name theme => theme.name === this.selectedTheme.name
) )
) { ) {
await this.$store.dispatch("resetTheme"); await this.$store.dispatch("resetTheme");

View File

@ -56,7 +56,7 @@
</template> </template>
<script> <script>
import api from "../../../api"; import api from "@/api";
import TimePickerDialog from "./TimePickerDialog"; import TimePickerDialog from "./TimePickerDialog";
export default { export default {
components: { components: {

View File

@ -49,7 +49,7 @@
</template> </template>
<script> <script>
import api from "../../api"; import api from "@/api";
export default { export default {
data() { data() {

View File

@ -44,14 +44,14 @@
color="primary" color="primary"
block="block" block="block"
type="submit" type="submit"
>{{$t('login.sign-in')}}</v-btn >{{ $t("login.sign-in") }}</v-btn
> >
<v-btn <v-btn
v-else v-else
block="block" block="block"
type="submit" type="submit"
@click.prevent="options.isLoggingIn = true" @click.prevent="options.isLoggingIn = true"
>{{$t('login.sign-up')}}</v-btn >{{ $t("login.sign-up") }}</v-btn
> >
</v-form> </v-form>
</v-card-text> </v-card-text>
@ -72,7 +72,7 @@
</template> </template>
<script> <script>
import api from "../../api"; import api from "@/api";
export default { export default {
props: {}, props: {},
data() { data() {

View File

@ -40,7 +40,7 @@
</template> </template>
<script> <script>
import utils from "../../utils"; import utils from "@/utils";
export default { export default {
props: { props: {
name: String, name: String,

View File

@ -12,6 +12,8 @@
hide-no-data hide-no-data
cache-items cache-items
solo solo
autofocus
auto-select-first
> >
<template <template
v-if="showResults" v-if="showResults"
@ -43,7 +45,7 @@
<script> <script>
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import utils from "../../utils"; import utils from "@/utils";
export default { export default {
props: { props: {

View File

@ -9,7 +9,7 @@
</template> </template>
<script> <script>
import api from "../../api"; import api from "@/api";
export default { export default {
props: { props: {
url: String, url: String,

View File

@ -5,7 +5,9 @@ import store from "./store/store";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import { routes } from "./routes"; import { routes } from "./routes";
import i18n from "./i18n"; import i18n from "./i18n";
import FlashMessage from "@smartweb/vue-flash-message";
Vue.use(FlashMessage);
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(VueRouter); Vue.use(VueRouter);
@ -14,16 +16,16 @@ const router = new VueRouter({
mode: process.env.NODE_ENV === "production" ? "history" : "hash", mode: process.env.NODE_ENV === "production" ? "history" : "hash",
}); });
new Vue({ const vueApp = new Vue({
vuetify, vuetify,
store, store,
router, router,
i18n, i18n,
render: (h) => h(App), render: h => h(App),
}).$mount("#app"); }).$mount("#app");
// Truncate // Truncate
let truncate = function (text, length, clamp) { let truncate = function(text, length, clamp) {
clamp = clamp || "..."; clamp = clamp || "...";
let node = document.createElement("div"); let node = document.createElement("div");
node.innerHTML = text; node.innerHTML = text;
@ -31,11 +33,12 @@ let truncate = function (text, length, clamp) {
return content.length > length ? content.slice(0, length) + clamp : content; return content.length > length ? content.slice(0, length) + clamp : content;
}; };
let titleCase = function (value) { let titleCase = function(value) {
return value.replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase()); return value.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
}; };
Vue.filter("truncate", truncate); Vue.filter("truncate", truncate);
Vue.filter("titleCase", titleCase); Vue.filter("titleCase", titleCase);
export { vueApp };
export { router }; export { router };

View File

@ -13,7 +13,7 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import CardSection from "../components/UI/CardSection"; import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar"; import CategorySidebar from "../components/UI/CategorySidebar";
export default { export default {

View File

@ -21,7 +21,7 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import CardSection from "../components/UI/CardSection"; import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar"; import CategorySidebar from "../components/UI/CategorySidebar";
export default { export default {
@ -55,7 +55,7 @@ export default {
}, },
methods: { methods: {
async buildPage() { async buildPage() {
this.homeCategories.forEach(async (element) => { this.homeCategories.forEach(async element => {
let recipes = await this.getRecipeByCategory(element.slug); let recipes = await this.getRecipeByCategory(element.slug);
recipes.position = element.position; recipes.position = element.position;
this.recipeByCategory.push(recipes); this.recipeByCategory.push(recipes);

View File

@ -74,8 +74,8 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import utils from "../utils"; import utils from "@/utils";
import NewMeal from "../components/MealPlan/MealPlanNew"; import NewMeal from "../components/MealPlan/MealPlanNew";
import EditPlan from "../components/MealPlan/MealPlanEditor"; import EditPlan from "../components/MealPlan/MealPlanEditor";

View File

@ -49,8 +49,8 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import utils from "../utils"; import utils from "@/utils";
export default { export default {
data() { data() {
return { return {

View File

@ -39,7 +39,7 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import RecipeEditor from "../components/Recipe/RecipeEditor"; import RecipeEditor from "../components/Recipe/RecipeEditor";
import VJsoneditor from "v-jsoneditor"; import VJsoneditor from "v-jsoneditor";

View File

@ -56,8 +56,8 @@
</template> </template>
<script> <script>
import api from "../api"; import api from "@/api";
import utils from "../utils"; import utils from "@/utils";
import VJsoneditor from "v-jsoneditor"; import VJsoneditor from "v-jsoneditor";
import RecipeViewer from "../components/Recipe/RecipeViewer"; import RecipeViewer from "../components/Recipe/RecipeViewer";
import RecipeEditor from "../components/Recipe/RecipeEditor"; import RecipeEditor from "../components/Recipe/RecipeEditor";
@ -107,7 +107,7 @@ export default {
}, },
watch: { watch: {
$route: function () { $route: function() {
this.getRecipeDetails(); this.getRecipeDetails();
}, },
}, },

View File

@ -44,7 +44,7 @@ import General from "../components/Settings/General";
import Webhooks from "../components/Settings/Webhook"; import Webhooks from "../components/Settings/Webhook";
import Theme from "../components/Settings/Theme"; import Theme from "../components/Settings/Theme";
import Migration from "../components/Settings/Migration"; import Migration from "../components/Settings/Migration";
import api from "../api"; import api from "@/api";
import axios from "axios"; import axios from "axios";
export default { export default {

View File

@ -6,6 +6,8 @@ Vue.use(Vuetify);
const vuetify = new Vuetify({ const vuetify = new Vuetify({
theme: { theme: {
dark: false, dark: false,
options: { customProperties: true },
themes: { themes: {
light: { light: {
primary: "#E58325", primary: "#E58325",

View File

@ -8,7 +8,7 @@ import AllRecipesPage from "./pages/AllRecipesPage";
import CategoryPage from "./pages/CategoryPage"; import CategoryPage from "./pages/CategoryPage";
import MeaplPlanPage from "./pages/MealPlanPage"; import MeaplPlanPage from "./pages/MealPlanPage";
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage"; import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
import api from "./api"; import api from "@/api";
export const routes = [ export const routes = [
{ path: "/", component: HomePage }, { path: "/", component: HomePage },
@ -24,7 +24,7 @@ export const routes = [
{ {
path: "/meal-plan/today", path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => { beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then((redirect) => { await todaysMealRoute().then(redirect => {
next(redirect); next(redirect);
}); });
}, },

View File

@ -1,4 +1,4 @@
import api from "../../api"; import api from "@/api";
const state = { const state = {
showRecent: true, showRecent: true,
@ -30,10 +30,10 @@ const actions = {
}; };
const getters = { const getters = {
getShowRecent: (state) => state.showRecent, getShowRecent: state => state.showRecent,
getShowLimit: (state) => state.showLimit, getShowLimit: state => state.showLimit,
getCategories: (state) => state.categories, getCategories: state => state.categories,
getHomeCategories: (state) => state.homeCategories, getHomeCategories: state => state.homeCategories,
}; };
export default { export default {

View File

@ -1,4 +1,4 @@
import api from "../../api"; import api from "@/api";
import Vuetify from "../../plugins/vuetify"; import Vuetify from "../../plugins/vuetify";
function inDarkMode(payload) { function inDarkMode(payload) {
@ -60,9 +60,9 @@ const actions = {
}; };
const getters = { const getters = {
getActiveTheme: (state) => state.activeTheme, getActiveTheme: state => state.activeTheme,
getDarkMode: (state) => state.darkMode, getDarkMode: state => state.darkMode,
getIsDark: (state) => state.isDark, getIsDark: state => state.isDark,
}; };
export default { export default {

View File

@ -1,6 +1,6 @@
import Vue from "vue"; import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
import api from "../api"; import api from "@/api";
import createPersistedState from "vuex-persistedstate"; import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings"; import userSettings from "./modules/userSettings";
import language from "./modules/language"; import language from "./modules/language";
@ -64,11 +64,11 @@ const store = new Vuex.Store({
getters: { getters: {
// //
getSnackText: (state) => state.snackText, getSnackText: state => state.snackText,
getSnackActive: (state) => state.snackActive, getSnackActive: state => state.snackActive,
getSnackType: (state) => state.snackType, getSnackType: state => state.snackType,
getRecentRecipes: (state) => state.recentRecipes, getRecentRecipes: state => state.recentRecipes,
}, },
}); });

View File

@ -1,6 +1,15 @@
// import utils from "../../utils"; // import utils from "@/utils";
// import Vue from "vue"; // import Vue from "vue";
// import Vuetify from "./plugins/vuetify"; // import Vuetify from "./plugins/vuetify";
import { vueApp } from "./main";
const notifyHelpers = {
baseCSS: "notify-base",
error: "notify-error-color",
warning: "notify-warning-color",
success: "notify-success-color",
info: "notify-info-color",
};
const days = [ const days = [
"Sunday", "Sunday",
@ -72,4 +81,28 @@ export default {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}, },
notify: {
show: function(text, type = "info", title = null) {
vueApp.flashMessage.show({
status: type,
title: title,
message: text,
time: 3000,
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
});
},
info: function(text, title = null) {
this.show(text, "info", title);
},
success: function(text, title = null) {
this.show(text, "success", title);
},
error: function(text, title = null) {
this.show(text, "error", title);
},
warning: function(text, title = null) {
this.show(text, "warning", title);
},
},
}; };

View File

@ -12,7 +12,6 @@ from routes import (
setting_routes, setting_routes,
static_routes, static_routes,
theme_routes, theme_routes,
user_routes,
) )
from routes.recipe import ( from routes.recipe import (
all_recipe_routes, all_recipe_routes,
@ -20,20 +19,9 @@ from routes.recipe import (
recipe_crud_routes, recipe_crud_routes,
tag_routes, tag_routes,
) )
from services.settings_services import default_settings_init
from utils.logger import logger 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( app = FastAPI(
title="Mealie", title="Mealie",
description="A place for all your recipes", description="A place for all your recipes",
@ -51,6 +39,11 @@ def start_scheduler():
import services.scheduler.scheduled_jobs import services.scheduler.scheduled_jobs
def init_settings():
default_settings_init()
import services.theme_services
def api_routers(): def api_routers():
# Recipes # Recipes
app.include_router(all_recipe_routes.router) app.include_router(all_recipe_routes.router)
@ -64,8 +57,6 @@ def api_routers():
app.include_router(theme_routes.router) app.include_router(theme_routes.router)
# Backups/Imports Routes # Backups/Imports Routes
app.include_router(backup_routes.router) app.include_router(backup_routes.router)
# User Routes
app.include_router(user_routes.router)
# Migration Routes # Migration Routes
app.include_router(migration_routes.router) app.include_router(migration_routes.router)
app.include_router(debug_routes.router) app.include_router(debug_routes.router)
@ -90,6 +81,7 @@ app.include_router(static_routes.router)
# generate_api_docs(app) # generate_api_docs(app)
start_scheduler() start_scheduler()
init_settings()
if __name__ == "__main__": if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----") logger.info("-----SYSTEM STARTUP-----")

View File

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

View File

@ -9,7 +9,6 @@ from db.sql.theme_models import SiteThemeModel
""" """
# TODO # TODO
- [ ] Abstract Classes to use save_new, and update from base models - [ ] 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.primary_key = "slug"
self.sql_model = RecipeModel 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: RecipeModel = self._query_one(session, match_value=slug)
entry.image = f"{slug}.{extension}" entry.image = f"{slug}.{extension}"
session.commit() session.commit()
@ -49,13 +48,14 @@ class _Settings(BaseDocument):
self.primary_key = "name" self.primary_key = "name"
self.sql_model = SiteSettingsModel 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) new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings) session.add(new_settings)
return_data = new_settings.dict()
session.commit() session.commit()
return new_settings.dict() return return_data
class _Themes(BaseDocument): class _Themes(BaseDocument):

View File

@ -106,7 +106,7 @@ class BaseDocument:
return db_entry 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. """Creates a new database entry for the given SQL Alchemy Model.
Args: \n 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,37 +1,79 @@
from typing import List, Optional import datetime
from typing import Any, List, Optional
import pydantic from pydantic import BaseModel, validator
from pydantic.main import BaseModel 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: class Config:
schema_extra = { schema_extra = {
"example": [ "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": [
{ {
"slug": "crockpot-buffalo-chicken", "text": "Season chicken with salt and pepper.",
"image": "crockpot-buffalo-chicken.jpg",
"name": "Crockpot Buffalo Chicken",
}, },
{ ],
"slug": "downtown-marinade", "slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"image": "downtown-marinade.jpg", "tags": ["favorite", "yummy!"],
"name": "Downtown Marinade", "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",
"slug": "detroit-style-pepperoni-pizza", "rating": 3,
"image": "detroit-style-pepperoni-pizza.jpg", "extras": {"message": "Don't forget to defrost the chicken!"},
"name": "Detroit-Style Pepperoni Pizza",
},
{
"slug": "crispy-carrots",
"image": "crispy-carrots.jpg",
"name": "Crispy Carrots",
},
]
} }
}
@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): class AllRecipeRequest(BaseModel):

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) @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.""" """Generates a backup of the recipe database in json format."""
export_path = backup_all( export_path = backup_all(
session=db, session=session,
tag=data.tag, tag=data.tag,
templates=data.templates, templates=data.templates,
export_recipes=data.options.recipes, export_recipes=data.options.recipes,
@ -66,7 +66,7 @@ def upload_backup_zipfile(archive: UploadFile = File(...)):
@router.get("/{file_name}/download") @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 """ """ Upload a .zip File to later be imported into Mealie """
file = BACKUP_DIR.joinpath(file_name) 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) @router.post("/{file_name}/import", status_code=200)
def import_database( 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 a database backup file generated from Mealie. """
import_db = ImportDatabase( import_db = ImportDatabase(
session=db, session=session,
zip_archive=import_data.name, zip_archive=import_data.name,
import_recipes=import_data.recipes, import_recipes=import_data.recipes,
force_import=import_data.force, 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"), 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 """ """ Doc Str """
with open(LOGGER_FILE, "rb") as f: with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num) log_text = tail(f, num)
HTML_RESPONSE = f""" HTML_RESPONSE = log_text
<html>
<head>
<title>Mealie Log</title>
</head>
<body style="white-space: pre-line">
<p>
{log_text}
</p>
</body>
</html>
"""
return HTML_RESPONSE 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]) @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 """ """ Returns a list of all available Meal Plan """
return MealPlan.get_all(db) return MealPlan.get_all(session)
@router.post("/create") @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 """ """ Creates a meal plan database entry """
data.process_meals(db) data.process_meals(session)
data.save_to_db(db) data.save_to_db(session)
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
# )
return SnackResponse.success("Mealplan Created") return SnackResponse.success("Mealplan Created")
@router.get("/this-week", response_model=MealPlan) @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 """ """ Returns the meal plan data for this week """
return MealPlan.this_week(db) return MealPlan.this_week(session)
@router.put("/{plan_id}") @router.put("/{plan_id}")
def update_meal_plan( 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 """ """ Updates a meal plan based off ID """
meal_plan.process_meals(db) meal_plan.process_meals(session)
meal_plan.update(db, plan_id) meal_plan.update(session, 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") return SnackResponse.info("Mealplan Updated")
@router.delete("/{plan_id}") @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 """ """ 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"]) @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. Returns the recipe slug for the meal scheduled for today.
If no meal is scheduled nothing is returned 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") @router.post("/{type}/{file_name}/import")
def import_nextcloud_directory( 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 """ """ Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name) file_path = MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud": if type == "nextcloud":
return nextcloud_migrate(db, file_path) return nextcloud_migrate(session, file_path)
elif type == "chowdown": elif type == "chowdown":
return chowdow_migrate(db, file_path) return chowdow_migrate(session, file_path)
else: else:
return SnackResponse.error("Incorrect Migration Type Selected") return SnackResponse.error("Incorrect Migration Type Selected")
@ -62,7 +62,7 @@ def delete_migration_data(type: str, file_name: str):
else: else:
SnackResponse.error("File/Folder not found.") 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") @router.post("/{type}/upload")

View File

@ -4,6 +4,8 @@ from fastapi import APIRouter, Depends
from models.category_models import RecipeCategoryResponse from models.category_models import RecipeCategoryResponse
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter( router = APIRouter(
prefix="/api/categories", prefix="/api/categories",
tags=["Recipe Categories"], tags=["Recipe Categories"],
@ -33,3 +35,5 @@ async def delete_recipe_category(
from any recipes that contain it """ from any recipes that contain it """
db.categories.delete(session, category) 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") 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") @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 """ """ Takes in a recipe slug, returns the static image """
recipe_image = read_image(recipe_slug) recipe_image = read_image(recipe_slug)
@ -75,10 +75,13 @@ def get_recipe_img(recipe_slug: str):
@router.put("/{recipe_slug}/image") @router.put("/{recipe_slug}/image")
def update_recipe_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. """ """ Removes an existing image and replaces it with the incoming file. """
response = write_image(recipe_slug, image, extension) response = write_image(recipe_slug, image, extension)
Recipe.update_image(recipe_slug, extension) Recipe.update_image(session, recipe_slug, extension)
return response return response

View File

@ -3,6 +3,8 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"]) router = APIRouter(tags=["Recipes"])
router = APIRouter( router = APIRouter(
@ -30,3 +32,5 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
from any recipes that contain it""" from any recipes that contain it"""
db.tags.delete(session, tag) 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 db.db_setup import generate_session
from fastapi import APIRouter, Depends 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 sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks from utils.post_webhooks import post_webhooks
from utils.snackbar import SnackResponse from utils.snackbar import SnackResponse
@ -9,10 +11,24 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
@router.get("") @router.get("")
def get_main_settings(db: Session = Depends(generate_session)): def get_main_settings(session: Session = Depends(generate_session)):
""" Returns basic site settings """ """ 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") @router.post("/webhooks/test")
@ -20,20 +36,3 @@ def test_webhooks():
""" Run the function to test your webhooks """ """ Run the function to test your webhooks """
return post_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("/") @router.get("/")
def root(): async def root():
return FileResponse(BASE_HTML) return FileResponse(BASE_HTML)
@router.get("/{full_path:path}") @router.get("/{full_path:path}")
def root_plus(full_path): async def root_plus(full_path):
return FileResponse(BASE_HTML) return FileResponse(BASE_HTML)

View File

@ -1,64 +1,47 @@
from db.db_setup import generate_session from db.db_setup import generate_session
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from services.settings_services import SiteTheme from models.theme_models import SiteTheme
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse from utils.snackbar import SnackResponse
from db.database import db
router = APIRouter(prefix="/api", tags=["Themes"]) router = APIRouter(prefix="/api", tags=["Themes"])
@router.get("/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 """ """ Returns all site themes """
return SiteTheme.get_all(db) return db.themes.get_all(session)
@router.post("/themes/create") @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 """ """ Creates a site color theme database entry """
data.save_to_db(db) db.themes.create(session, data.dict())
# try:
# data.save_to_db()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
# )
return SnackResponse.success("Theme Saved") return SnackResponse.success("Theme Saved")
@router.get("/themes/{theme_name}") @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 """ """ Returns a named theme """
return SiteTheme.get_by_name(db, theme_name) return db.themes.get(session, theme_name)
@router.put("/themes/{theme_name}") @router.put("/themes/{theme_name}")
def update_theme( 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 """ """ Update a theme database entry """
data.update_document(db) db.themes.update(session, theme_name, data.dict())
# try: return SnackResponse.info(f"Theme Updated: {theme_name}")
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
# )
return SnackResponse.success("Theme Updated")
@router.delete("/themes/{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 """ """ Deletes theme from the database """
SiteTheme.delete_theme(db, theme_name) db.themes.delete(session, 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") 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 pathlib import Path
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR 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 db.db_setup import create_session
from jinja2 import Template from jinja2 import Template
from services.meal_services import MealPlan from services.meal_services import MealPlan
from services.recipe_services import Recipe from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from utils.logger import logger from utils.logger import logger
@ -88,20 +88,18 @@ class ExportDatabase:
shutil.copy(file, self.img_dir.joinpath(file.name)) shutil.copy(file, self.img_dir.joinpath(file.name))
def export_settings(self): 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") 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): def export_themes(self):
all_themes = SiteTheme.get_all(self.session) all_themes = db.themes.get_all(self.session)
if all_themes: if all_themes:
all_themes = [x.dict() for x in all_themes]
out_file = self.themes_dir.joinpath("themes.json") out_file = self.themes_dir.joinpath("themes.json")
ExportDatabase._write_json_file(all_themes, out_file) ExportDatabase._write_json_file(all_themes, out_file)
def export_meals( def export_meals(self):
self, #! Problem Parseing Datetime Objects... May come back to this
): #! Problem Parseing Datetime Objects... May come back to this
meal_plans = MealPlan.get_all(self.session) meal_plans = MealPlan.get_all(self.session)
if meal_plans: if meal_plans:
meal_plans = [x.dict() for x in 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) ExportDatabase._write_json_file(meal_plans, out_file)
@staticmethod @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) json_data = json.dumps(data, indent=4, default=str)
with open(out_file, "w") as f: with open(out_file, "w") as f:

View File

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

View File

@ -8,19 +8,6 @@ from sqlalchemy.orm.session import Session
from services.recipe_services import Recipe 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): class Meal(BaseModel):
slug: Optional[str] slug: Optional[str]
@ -81,7 +68,7 @@ class MealPlan(BaseModel):
self.meals = meals self.meals = meals
def save_to_db(self, session: Session): def save_to_db(self, session: Session):
db.meals.save_new(session, self.dict()) db.meals.create(session, self.dict())
@staticmethod @staticmethod
def get_all(session: Session) -> List: def get_all(session: Session) -> List:

View File

@ -1,5 +1,4 @@
import datetime import datetime
import json
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional from typing import Any, List, Optional
@ -98,13 +97,7 @@ class Recipe(BaseModel):
except: except:
recipe_dict["image"] = "no image" recipe_dict["image"] = "no image"
# try: recipe_doc = db.recipes.create(session, recipe_dict)
# total_time = recipe_dict.get("totalTime")
# recipe_dict["totalTime"] = str(total_time)
# except:
# pass
recipe_doc = db.recipes.save_new(session, recipe_dict)
recipe = Recipe(**recipe_doc) recipe = Recipe(**recipe_doc)
return recipe.slug return recipe.slug
@ -122,7 +115,7 @@ class Recipe(BaseModel):
return updated_slug.get("slug") return updated_slug.get("slug")
@staticmethod @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 """A helper function to pass the new image name and extension
into the database. into the database.
@ -130,11 +123,8 @@ class Recipe(BaseModel):
slug (str): The current recipe slug slug (str): The current recipe slug
extension (str): the file extension of the new image 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 @staticmethod
def get_all(session: Session): def get_all(session: Session):
return db.recipes.get_all(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.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser from services.scheduler.scheduler_utils import Cron, cron_parser
from services.settings_services import SiteSettings
from utils.logger import logger from utils.logger import logger
from models.settings_models import SiteSettings
from db.database import db
from utils.post_webhooks import post_webhooks 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 poll the database for changes and reschedule the webhook time
""" """
session = create_session() 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) time = cron_parser(settings.webhooks.webhookTime)
job = JOB_STORE.get("webhooks") 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.image_services import scrape_image
from services.recipe_services import Recipe 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): 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: def extract_recipe_from_html(html: str, url: str) -> dict:
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True) scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
dump_last_json(scraped_recipes)
if not scraped_recipes: if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url( 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: def basic_recipe_from_opengraph(html: str, url: str) -> dict:
base_url = get_base_url(html, url) base_url = get_base_url(html, url)
data = extruct.extract(html, base_url=base_url) data = extruct.extract(html, base_url=base_url)
try:
properties = data["opengraph"][0]["properties"] properties = data["opengraph"][0]["properties"]
except:
return
return { return {
"name": og_field(properties, "og:title"), "name": og_field(properties, "og:title"),
"description": og_field(properties, "og:description"), "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: def process_recipe_url(url: str) -> dict:
r = requests.get(url) r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, 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: def create_from_url(url: str) -> Recipe:
recipe_data = process_recipe_url(url) 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) recipe = Recipe(**recipe_data)
return recipe return recipe

View File

@ -1,149 +1,16 @@
from typing import List, Optional
from db.database import db from db.database import db
from db.db_setup import create_session, sql_exists 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 sqlalchemy.orm.session import Session
from utils.logger import logger
class Webhooks(BaseModel): def default_settings_init(session: Session = None):
webhookTime: str = "00:00" if session == None:
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() session = create_session()
try: 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() webhooks = Webhooks()
default_entry = SiteSettings(name="main", 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 db.db_setup import generate_session, sql_global_init
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pytest import fixture 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 from tests.test_config import TEST_DATA
@ -18,13 +20,13 @@ TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
def override_get_db(): def override_get_db():
try: try:
db = TestSessionLocal() db = TestSessionLocal()
default_theme_init()
default_settings_init()
yield db yield db
finally: finally:
db.close() db.close()
@fixture(scope="session") @fixture(scope="session")
def api_client(): 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", "error": "#EF5350",
}, },
} }
api_client.post(THEMES_CREATE, json=default_theme) api_client.post(THEMES_CREATE, json=default_theme)
return default_theme return default_theme

View File

@ -65,20 +65,20 @@ def test_normalize_instructions(instructions):
] ]
def test_html_no_recipe_data(): # 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") # 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" # 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) # recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10 # assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10 # assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url # assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100 # assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"]) # assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"] # assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [ # assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"} # {"text": "Could not detect instructions"}
] # ]
def test_html_with_recipe_data(): def test_html_with_recipe_data():

View File

@ -1,15 +1,17 @@
import json import json
import requests import requests
from db.database import db
from db.db_setup import create_session from db.db_setup import create_session
from models.settings_models import SiteSettings
from services.meal_services import MealPlan from services.meal_services import MealPlan
from services.recipe_services import Recipe from services.recipe_services import Recipe
from services.settings_services import SiteSettings
def post_webhooks(): def post_webhooks():
session = create_session() 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: if all_settings.webhooks.enabled:
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict() todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()

View File

@ -9,18 +9,6 @@ class SnackResponse:
return snackbar 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 @staticmethod
def success(message: str, additional_data: dict = None) -> dict: def success(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "success", additional_data) return SnackResponse._create_response(message, "success", additional_data)