File: README.md
Tandoor Recipes
The recipe manager that allows you to manage your ever growing collection of digital recipes.
Core Features
- π₯ Manage your recipes - Manage your ever growing recipe collection
- π Plan - multiple meals for each day
- π Shopping lists - via the meal plan or straight from recipes
- πͺ use AI to recognize images, sort recipe steps, find nutrition facts and more
- π Cookbooks - collect recipes into books
- πͺ Share and collaborate on recipes with friends and family
Made by and for power users
- π Powerful & customizable search with fulltext support and TrigramSimilarity
- π·οΈ Create and search for tags, assign them in batch to all files matching certain filters
- βοΈ Quickly merge and rename ingredients, tags and units
- π₯οΈ Import recipes from thousands of websites supporting ld+json or microdata
- β Support for fractions or decimals
- π³ Easy setup with Docker and included examples for Kubernetes, Unraid and Synology
- π¨ Customize your interface with themes
- π¦ Sync files with Dropbox and Nextcloud
All the must haves
- π±Optimized for use on mobile devices
- π localized in many languages thanks to the awesome community
- π₯οΈ Import your collection from many other recipe managers
- β Many more like recipe scaling, image compression, printing views and supermarkets
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
a public page.
Docs
Documentation can be found here.
β€οΈ Support our work β€οΈ
Tandoor is developed by volunteers in their free time just because its fun. That said earning
some money with the project allows us to spend more time on it and thus make improvements we otherwise couldn't.
Because of that there are several ways you can support us
- GitHub Sponsors You can sponsor contributors of this project on GitHub: vabene1111
- Patron You can sponsor contributors of this project on Patron: vabene111
- Host at Hetzner We have been very happy customers of Hetzner for multiple years for all of our projects. If you want to get into self-hosting or are tired of the expensive big providers, their cloud servers are a great place to get started. When you sign up via our referral link you will get 20β¬ worth of cloud credits and we get a small kickback too.
- Let us host for you We are offering a hosted version where all profits support us and the development of tandoor (currently only available in germany).
Contributing
Contributions are welcome but please read this BEFORE contributing anything!
Your Feedback
Share some information on how you use Tandoor to help me improve the application Google Survey
Get in touch
| Community | Get support, share best practices, discuss feature ideas, and meet other Tandoor users. |
|---|---|
| Discord | We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements |
License
Beginning with version 0.10.0 the code in this repository is licensed under the GNU AGPL v3 license with a
common clause selling exception. See LICENSE.md for details.
NOTE: There appears to be a whole range of legal issues with licensing anything other than the standard completely open licenses.
I am in the process of getting some professional legal advice to sort out these issues.
Please also see Issue 238 for some discussion and reasoning regarding the topic.
Reasoning
This software and all its features are and will always be free for everyone to use and enjoy.
The reason for the selling exception is that a significant amount of time was spend over multiple years to develop this software.
A paid hosted version which will be identical in features and code base to the software offered in this repository will
likely be released in the future (including all features needed to sell a hosted version as they might also be useful for personal use).
This will not only benefit me personally but also everyone who self-hosts this software as any profits made through selling the hosted option
allow me to spend more time developing and improving the software for everyone. Selling exceptions are approved by Richard Stallman and the
common clause license is very permissive (see the FAQ).
File: docs/contribute/feature_contrib/featureguides.md
If there is a not a guide below for the type of feature you wish to implement then please add a request on github for the type of feature you wish to contribute.
Alternatively please document the process of adding a feature type if you have already contributed a feature.
Feature Contribution Guides
File: docs/contribute/feature_contrib/Integration.md
Import / Export Integration Contribution Guide
Setup
There are 5 files you need to configure in order create a new integration
- Create a new integration class in
/cookbook/integration/ - Include integration in
/cookbook/forms.py - Add the new integration class import and include in the 'if' chain in
/cookbook/views/import_export.py - Add your integration to the documentation at
/docs/features/import_export.md - Include integration and docs link in
/vue3/src/utils/integration_utils.ts
1. Creating a New Integration Class
Your integration class should be named after what you are integrating with and should inherit from the Integration class. Use the template below to setup your class.
/cookbook/integration/yourintegration.py
/* Detailed source-code truncated for AI context efficiency. */2. including in Forms.py
In the /cookbook/forms.py find the ImportExportBase class and add to it the following amoung the others:
YOURINTEGRATION = "YOURINTEGRATION"Then you will find under that the following declaration:
type = forms.ChoiceField(choices=())
the choices will have a long list of tuples. Add to the list of tuples the following:
(YOURINTEGRATION, 'Your Integration'),3. Including in Views
In the /cookbook/views/import_export.py import your integration
from cookbook.integration.yourintegration import YourIntegrationThen add the following code to the long if chain:
if export_type == ImportExportBase.YOURINTEGRATION:
return YourIntegration(request, export_type)be careful to use the exact all caps name of your integration that you used in the cookbook/forms.py or else it won't recognize it as a type. the snake case is the class that you defined in /cookbook/integration
4. Add to the Documentation
If nothing else, you at least have to add one slugline about your integration in the /docs/features/import_export.md because the Vue pages require a link to send the send the user to if they hit the information button on the import/export form.
Go to the bottom of the doc and add:
## YourIntegration
a little blurb about how it works or anything users should know about how the data needs to be formated.Additionally add your integration to the table at the top of the document, marking the state of your integration, or wait until it is integrated and tested before adding to the table.
5. Add to Vue Integration Utils
In the /vue3/src/utils/integration_utils.ts find export const INTEGRATIONS: Array and in the long list add:
{id: 'YOURINTEGRATION', name: "Your Integration", import: true, export: false, helpUrl: 'https://docs.tandoor.dev/features/import_export/#yourintegration'},be sure to change 'true' or 'false' value for the import and export options to the correct values for your integration. 'true' indicates that it should be listed in the menu for imports or exports respectively.
Be sure to update vue using `yarn` after to be sure the html files get updated
Integration Test Setup
File Structure and Files
Tandoor uses Pytest to implement its testing features. To add tests and test documents navigate to
cookbook/tests/other/There you can create a test file test_yourintegration.py. It is very important that it starts with "test_" as that is how pytest knows to run it as a test.
For the text files that you will want to parse and test for your integration you can put them at:
cookbook/test/other/test_data/your_integration/making your own folder for your test inputs there. When directing your tests to pull the files from that folder make sure to include the whole path starting at cookbook/
Creating an Integration Test with a Request object
Like the filename, inside the file test_yourintegration.py pytest looks specifically for the functions that start with the string test_ any function that does not have that prefex won't be run. This is useful if you want to define helper methods to your tests.
Since your integration is a child of the Integration class you must pass your integration the required arguments: request and export_type. Unless your test has a specific export_type you are trying to test, it is not consequential what you put for export_type, so long as it is a string. I generally just use "export" in my test. For request though we need to generate one
You can accomplish this with the following code:
from io import BytesIO
from django.contrib import auth
from django.test import RequestFactory
from django_scopes import scope
from cookbook.integration.cooklang import YourIntegrationClass
def test_yourintegration(u1_s1):
user = auth.get_user(u1_s1)
space = user.userspace_set.first().space
request = RequestFactory()
request.user = user
request.space = space
with scope(space=space):
integration_object = YourIntegrationClass(request, "export")
with open("cookbook/test/other/test_data/your_integration/recipe_file.txt", "rb") as file:
recipe_bytes = file.read()
recipe_name = file.name
buffer = BytesIO(recipe_bytes)
buffer.name = recipe_name
recipe_object = integration_object.get_recipe_from_file(buffer)
# all of your test function logic inside this with clausethough it is not important for you to understand, the u1_s1 is a pytest fixture that can be passed into your tests. By adding it as an argument for the test, pytest will fill that fixture in and you can use it to get a test user using the auth.get_user() method
with that you can add your assertions and test it using pytest.
Integration Class Logic
Now that the setup is complete you need to implement the logic on the new Integration class you created.
get_recipe_from_file method
This function is called by the Integration.do_import() class method when a file is imported through the web portal. The get_recipe_from_file(self, file) takes only a file as an argument, which is passed from the Integration.do_import() as an IOByte binary object containing only the binary characters of the contents of the file, as well as a property called name that has a "utf-8" string of what the file name was.
Reading the File
To get the string values of the contents of the file, you must call the following methods:
file_text = file.getvalue().decode("utf-8")Then you can parse the file_text however you see fit for your integration. no need to do a with open() statement. The Integration parent class handles the opening and the closing of the file.
If your recipe name is dependent on the filename you can access the file name with:
filename = file.nameManaging the Tandoor 'Recipe' Object
The Recipe class has many properties, and it is recommended to view all of them here in order to determine specifically how your integration will organize its data into the Recipe object data structure. The important ones to know are:
name = models.CharField(max_length=128)
description = models.CharField(max_length=512, blank=True, null=True)
servings = models.IntegerField(default=1)
servings_text = models.CharField(default='', blank=True, max_length=32)
image = models.ImageField(upload_to='recipes/', blank=True, null=True)
keywords = models.ManyToManyField(Keyword, blank=True)
steps = models.ManyToManyField(Step, blank=True)
internal = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
space = models.ForeignKey(Space, on_delete=models.CASCADE)You can define as many of these variables as you like at the time of creating the Recipe object but the bare minimum should be:
from cookbook.models import Recipe
def get_recipe_from_file(self, file)-> Recipe:
#opening the file and parsing it
recipe_object = Recipe.objects.create(
name = your_recipe_name,
created_by = self.request.user,
internal = True,
space = self.request.space,
)
#translating the parsed file into th recipe object
return recipe_objectBoth created_by and space require the request object from the webpage or API that initiated the request, the Integration parent class handles both of those and they can be retrieved through the self.request property. The name allows the recipe to be identifiable from the other recipes, making it uniquely identifiable from other newly instantiated Recipe objects. lasty True is the default value for internal only change it to false if that is what you intend.
After the Recipe object is created you can change or add data to its properties, these changes will show up in tests and work as expected until the program drops it from memory. Then all those changes will be lost unless you use the .save() method. It is recommended not to use the .save() after every change, but instead to make all your changes and call .save() once at the end of the get_recipe_from_file method before the object gets returned back to the Integration.do_import() method.
from cookbook.models import Recipe
def get_recipe_from_file(self, file)-> Recipe:
#opening the file and parsing it
recipe_object = Recipe.objects.create(
name = your_recipe_name,
created_by = self.request.user,
internal = True,
space = self.request.space,
)
# Integration logic, making many changes to the recipe_object
recipe_object.description = "A yummy treat for any day!"
recipe_object.save()
return recipe_objectOther Models You Need To Know
The 3 main models other than Recipe you will need to use are the Step, Ingredient(also Food and Unit- They all go hand in hand), and Keyword. The rough structure of these object in a Recipe are:
(Note that the Recipe object is not a Json object, the below is just a representation of the data)
{Recipe:
[{"steps":
[{Step: [
{"ingredients": [
{Ingredient:
{"food":
{Food:
[{"name": "food name"},
{Unit: {"name": "unit name"}},
{"amount": int}
]}}}]},
{"instruction": "recipe step text"}
]}]},
{"keywords": [
{Keyword:
{"name": "keyword name"}
}]}]}Below are their bare-bones implementations in regard to creating a Recipe object integration.
from cookbook.models import Recipe, Keyword, Step, Ingredient, Food, Unit
def get_recipe_from_file(self, file)-> Recipe:
#opening the file and parsing it
recipe_object = Recipe.objects.create(
name = your_recipe_name,
created_by = self.request.user,
internal = True,
space = self.request.space,)
#logic for creating a list of keywords
for keyword in parsed_file.keywords:
recipe_object.keywords.add(
Keyword.objects.get_or_create(
space=self.request.space,
name=keyword)[0])
i = 0
for line in parsed_file:
#logic for creating a list of ingredients
#logic for instructions
step = Step.objects.create(
instruction=line.instruction_string,
order=i,
space=self.request.space,
show_ingredients_table=self.request.user.userpreference.show_step_ingredients)
for ingredient in line.ingredients:
step.ingredients.add(
Ingredient.objects.create(
food=Food.objects.get_or_create(name=ingredient.name, space=self.request.space)[0],
unit=Unit.objects.get_or_create(name=ingredient.quantity.unit, space=self.request.space)[0],
amount=ingredient.quantity.amount,
space=self.request.space, ))
recipe_object.steps.add(step)
i+=1
recipe_object.save()
return recipe_objectimport_file_name_filter method
Documentation to come.
get_file_from_recipe method
Documentation to come.
get_files_from_recipes method
Documentation to come.
File: docs/contribute/contribute.md
If you like this application and want it to give back, there are many ways to contribute.
!!! success "Contribution List"
If you help bring this project forward you deserve to be credited for it.
Feel free to add yourself to CONTRIBUTERS.md or message me to add you if you have contributed anything.
Translations
If you know any foreign languages you can:
Improve the translations for any of the existing languages.
See here for further information on how to contribute translation to Tandoor.
Issues and Feature Requests
The most basic but also crucial way of contributing is reporting issues and commenting on ideas and feature requests
over at GitHub issues.
Without feedback, improvement can't happen, so don't hesitate to say what you want to say.
Documentation
Helping improve the documentation for Tandoor is one of the easiest ways to give back and doesn't even require deep technical knowledge.
You can write guides on how to install and configure Tandoor expanding our repository of non-standard configuations.
Or you can write how-to guides using some of Tandoor's advanced features such as authentication or automation.
See here for more information on how to add documentation to Tandoor.
Contributing Code
For the truly ambitious, you can help write code to fix issues, add additional features, or write your own scripts using
Tandoor's extensive API and share your work with the community.
Guides for contributing specific types of features can be found here
Before writing any code, please make sure that you review contribution guidelines and
VSCode or PyCharm specific configurations.
File: docs/contribute/documentation.md
The documentation is built from the markdown files in the docs
folder of the GitHub repository.
In order to contribute to the documentation, there are a couple of methods that you can use.
Directly on GitHub
You can fork the develop repository and edit the markdown files directly on the GitHub website.
With an IDE
You can fork the develop repository and edit the markdown files from your favorite IDE such as VSCode or PyCharm.
One advantage of using as IDE is that you can preview your changes by:
Installing mkdocs
Now install mkdocs and dependencies: pip install mkdocs-material mkdocs-include-markdown-plugin.
Serving Documetation
If you want to test the documentation, locally run mkdocs serve from the project root.
Super Low Tech
Create your documentation in any work processor (or even create a video!) and open a feature request
attaching your document and requesting that someone add the documentation to Tandoor.
File: docs/contribute/guidelines.md
If you want to contribute bug fixes or small tweaks then your pull requests are always welcome!
!!! danger "Discuss First!"
If you want to contribute larger features that introduce more complexity to the project please
make sure to first submit a technical description outlining what and how you want to do it.
This allows me and the community to give feedback and manage the complexity of the overall
application. If you don't do this please don't be mad if I reject your PR.
License
Contributing to Tandoor requires signing a Contributor License Agreement. You can review the CLA here.
Linting & Formatting
Tandoor uses a number of libraries to maintain style and formatting consistency.
To contribute to the project you are required to use the following packages with the project defined configurations:
- flake8
- yapf
- isort
- prettier
!!! tip "Manual Formatting"
It is possible to run formatting manually, but it is recommended to setup your IDE to format on save.
bash flake8 file.py --ignore=E501 | isort -q file.py | yapf -i file.py prettier --write file.vue
Testing
Django uses pytest-django to implement a full suite of testing. If you make any functional changes, please implement the appropriate
tests.
Tandoor is also actively soliciting contributors willing to setup vue3 testing. If you have knowledge in this area it would be greatly appreciated.
API Client
!!! note "JAVA required"
The OpenAPI Generator is a Java project. You must have the java binary executable available on your PATH for this to work.
Tandoor uses django-rest-framework for API implementation. Making contributions that impact the API requires an understanding of
ViewSets and Serializers.
The API Client is generated automatically from the OpenAPI interface provided by the Django REST framework.
For this openapi-generator is used.
Install it using your desired setup method. (For example, using npm install @openapitools/openapi-generator-cli -g.)
Vue
Generate the schema using the generate_api_client.py script in the main directory.
Install and Configuration
Instructions for VSCode
Instructions for PyCharm
File: docs/contribute/installation.md
!!! info "Development Setup"
The dev setup is a little messy as this application combines the best (at least in my opinion) of both Django and Vue.js.
Devcontainer Setup
There is a devcontainer set up to ease development. It is optimized for VSCode, but should be able to
be used by other editors as well. Once the container is running, you can do things like start a Django dev server, start a Vue.js
dev server, run python tests, etc. by either using the VSCode tasks below, or manually running commands described in the individual
technology sections below.
In VSCode, simply check out the git repository, and then via the command palette, choose Dev Containers: Reopen in container.
If you need to change python dependencies (requierments.txt) or OS packages, you will need to rebuild the container. If you are
changing OS package requirements, you will need to update both the main Dockerfile and the .devcontainer/Dockerfile.
Django
This application is developed using the Django framework for Python. They have excellent
documentation on how to get started, so I will only give you the basics here.
- Clone this repository wherever you like and install the Python language for your OS (I recommend using version 3.10 or above).
- Open it in your favorite editor/IDE (e.g. PyCharm).
a. If you want, create a virtual environment for all your packages. - Install all required packages:
pip install -r requirements.txt. - Run the migrations:
python manage.py migrate. - Start the development server:
python manage.py runserver.
There is no need to set any environment variables. By default, a simple SQLite database is used and all settings are
populated from default values.
Vue.js
!!! danger "Development Setup"
The vite dev server must be started before the django runserver command is run or else django will not recognize it and try to fallback to the build files.
The frontend is build using Vue.js.
In order to work on these pages, you will have to install a Javascript package manager of your choice. The following examples use yarn.
- go to the
vue3and runyarn installto install the dependencies - run
yarn serveto start the dev server that allows hot reloading and easy and quick development
If you do not wish to work on those pages, but instead want the application to work properly during development, run yarn build to build the frontend pages once. After that you
might need to run python manage.py collectstatic to setup the static files.
Building Docker Image from Source
Similar to the Vue.js procedure, if you wish to build a docker image from source run you must build the vue3 files first.
- in your project files navigate to
vue3and runyarn installto install the dependencies - run
yarn buildto build the static files for django to use. - navigate back to the root directory and run
docker build -t ${tag}:${version} .
Django's collectstatic command is not necessary in this instance as the Dockerfile's entrypoint will collect the static files upon startup on docker run.
File: docs/contribute/pycharm.md
PyCharm can be configured to format and lint on save. Doing so requires some manual configuration as outlined below.
Setup File Watchers
- Navigate to File -> Settings -> Plugins
- Download and install File Watchers
- Navigate to File -> Settings -> Tools -> Black
- Confirm 'Use Black Formatter' is unchecked for both 'On code reformat' and 'On save'
Setup flake8 Watcher
Navigate to File -> Settings -> Tools -> File Watchers
Click the '+' to add a new watcher.
Configure the watcher as below.
Navigate to File -> Settings -> Editor -> Inspections -> File watcher problems
Under Severity select 'Edit Severities'
Click the '+' to add a severity calling it 'Linting Error'
Configure a background and effect as below.
Setup isort
- Navigate to File -> Settings -> Tools -> File Watchers
- Click the '+' to add a new watcher.
- Configure the watcher as below.
Setup yapf
- Navigate to File -> Settings -> Tools -> File Watchers
- Click the '+' to add a new watcher.
- Configure the watcher as below.
!!! hint
Adding a comma at the end of a list will trigger yapf to put each element of the list on a new line
!!! note
In order to debug vue yarn and vite servers must be started before starting the django server.
Setup prettier
- Navigate to File -> Settings -> Tools -> File Watchers
- Click the '+' to add a new watcher.
- Change 'File Type' to 'Any'.
- Click the three dots next to 'Scope' to create a custom scope.
- Click '+' to add a new scope
- Name: prettier
- Pattern:
file:vue/src//*||file:vue3/src//*||file:docs//*
- Configure the watcher as below.
- Arguments:
--cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$
File: docs/contribute/related.md
Recipe Scraper
While not directly related to Tandoor, we make extensive use of the brilliant recipe-scrapers
package by hhursev.
If you have the skills to add new sites or help resolve issues you are indirectly helping Tandoor.
Unofficial mobile apps
kitshn
Unofficial Tandoor recipes client
Maintained by Aimo
- Website: https://kitshn.app/
- Appstores: Apple, Android
Untare (discontinued)
Maintained by phantomate
GPT Recipe
Maintained by John Pedrie
Convert pictures of recipes to a structure that can be imported to Tandoor with ChatGPT.
Tandoor Menu Generator
Maintained by smilerz
Generate a meal plan based on complex criteria and optionally insert it into an SVG menu template.
Morsl
Maintained by featurecreep-cron
Automate meal planning for Tandoor. Set up profiles with filter rules β rating, books, tags, recently cooked β and generate meal plans that sync directly to Tandoor. Supports automatic generation and a shareable menu for household members.
File: docs/contribute/translations.md
Translations are managed on translate.tandoor.dev, a self hosted instance of Weblate.
You can simply register an account and then follow these steps to add translations:
- After registering, you are asked to select your languages. This is optional but allows weblate to only show you relevant translations.
- In the navigation click on
Projectsand thenBrowse all projects. - Select Tandoor and on the top-right hand corner, select
Watch project Tandoor(click onNot watching). - Go back to the dashboard. It now shows you the relevant translations for your languages. Click on the pencil icon to get started.
!!! info "Creating a new language"
To create a new language you must first select Tandoor (the project) and then a component.
Here you will have the option to add the language. Afterwards you can also simply add it to the other components as well.
Once a new language is (partially) finished let me know on GitHub so I can add it to the language-switcher in Tandoor itself.
There is also a lot of documentation available from Weblate directly.
It is also possible to provide the translations directly by creating a new language
using manage.py makemessages -l <language_code> -i venv. Once finished, simply open a PR with the changed files. This sometimes causes issues merging
with weblate, so I would prefer the use of weblate.