recipes Documentation

Application for managing recipes, planning meals, building shopping lists and much much more!

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy recipes on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name recipes --restart always vabene1111/recipes
Configured Services & Ports
βš™οΈ db_recipes
image: postgres:16-alpine
βš™οΈ web_recipes
image: vabene1111/recipes
docker-compose.yml
yaml (docker-compose.yml)
services:
  db_recipes:
    restart: always
    image: postgres:16-alpine
    volumes:
      - ./postgresql:/var/lib/postgresql/data
    env_file:
      - ./.env
    networks:
      - default

  web_recipes:
    restart: always
    image: vabene1111/recipes
    env_file:
      - ./.env
    volumes:
      - staticfiles:/opt/recipes/staticfiles
      - ./mediafiles:/opt/recipes/mediafiles
    depends_on:
      - db_recipes
    labels: # traefik example labels
      - "traefik.enable=true"
      - "traefik.http.routers.recipes.rule=Host(`recipes.mydomain.com`, `recipes.myotherdomain.com`)"
      - "traefik.http.routers.recipes.entrypoints=web_secure" # your https endpoint
      - "traefik.http.routers.recipes.tls.certresolver=le_resolver" # your cert resolver
    networks:
      - default
      - traefik


networks:
  default:
  traefik: # This is your external traefik network
    external: true

volumes:
  staticfiles:

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

  1. Create a new integration class in /cookbook/integration/
  2. Include integration in /cookbook/forms.py
  3. Add the new integration class import and include in the 'if' chain in /cookbook/views/import_export.py
  4. Add your integration to the documentation at /docs/features/import_export.md
  5. 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

text
/* 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:

python
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:

python
(YOURINTEGRATION, 'Your Integration'),

3. Including in Views

In the /cookbook/views/import_export.py import your integration

python
from cookbook.integration.yourintegration import YourIntegration

Then add the following code to the long if chain:

python
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:

markdown
## 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:

typo3_typoscript
{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

text
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:

text
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:

python
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 clause

though 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:

python
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:

python
filename = file.name

Managing 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:

python
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:

python
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_object

Both 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.

python
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_object

Other 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)

json
{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.

python
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_object

import_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.

  1. Clone this repository wherever you like and install the Python language for your OS (I recommend using version 3.10 or above).
  2. Open it in your favorite editor/IDE (e.g. PyCharm).
    a. If you want, create a virtual environment for all your packages.
  3. Install all required packages: pip install -r requirements.txt.
  4. Run the migrations: python manage.py migrate.
  5. 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.

  1. go to the vue3 and run yarn install to install the dependencies
  2. run yarn serve to 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.

  1. in your project files navigate to vue3 and run yarn install to install the dependencies
  2. run yarn build to build the static files for django to use.
  3. 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

  1. Navigate to File -> Settings -> Plugins
  2. Download and install File Watchers
  3. Navigate to File -> Settings -> Tools -> Black
  4. Confirm 'Use Black Formatter' is unchecked for both 'On code reformat' and 'On save'

Setup flake8 Watcher

  1. Navigate to File -> Settings -> Tools -> File Watchers

  2. Click the '+' to add a new watcher.

  3. Configure the watcher as below.

  4. Navigate to File -> Settings -> Editor -> Inspections -> File watcher problems

  5. Under Severity select 'Edit Severities'

  6. Click the '+' to add a severity calling it 'Linting Error'

  7. Configure a background and effect as below.

Setup isort

  1. Navigate to File -> Settings -> Tools -> File Watchers
  2. Click the '+' to add a new watcher.
  3. Configure the watcher as below.

Setup yapf

  1. Navigate to File -> Settings -> Tools -> File Watchers
  2. Click the '+' to add a new watcher.
  3. 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

  1. Navigate to File -> Settings -> Tools -> File Watchers
  2. Click the '+' to add a new watcher.
  3. Change 'File Type' to 'Any'.
  4. Click the three dots next to 'Scope' to create a custom scope.
  5. Click '+' to add a new scope
  • Name: prettier
  • Pattern: file:vue/src//*||file:vue3/src//*||file:docs//*
  1. Configure the watcher as below.
  • Arguments: --cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$

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

Untare (discontinued)

Maintained by phantomate

iPhone
Android

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:

  1. After registering, you are asked to select your languages. This is optional but allows weblate to only show you relevant translations.
  2. In the navigation click on Projects and then Browse all projects.
  3. Select Tandoor and on the top-right hand corner, select Watch project Tandoor (click on Not watching).
  4. 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.