Full Code of TandoorRecipes/recipes for AI

develop be4301e79b04 cached
1489 files
22.5 MB
6.0M tokens
10776 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (23,836K chars total). Download the full file to get everything.
Repository: TandoorRecipes/recipes
Branch: develop
Commit: be4301e79b04
Files: 1489
Total size: 22.5 MB

Directory structure:
gitextract_yzw4jm7n/

├── .coveragerc
├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .dockerignore
├── .flake8
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md.bak
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── doc_issue.yml
│   │   ├── feature_request.md.bak
│   │   ├── feature_request.yml
│   │   ├── help-request.md.bak
│   │   ├── help_request.yml
│   │   ├── url_import.md.bak
│   │   └── website_import.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-docker.yml
│       ├── ci.yml
│       ├── codeql-analysis.yml
│       └── docs.yml
├── .gitignore
├── .idea/
│   ├── codeStyles/
│   │   └── codeStyleConfig.xml
│   ├── dictionaries/
│   │   ├── vaben.xml
│   │   └── vabene1111_PC.xml
│   ├── inspectionProfiles/
│   │   └── profiles_settings.xml
│   ├── modules.xml
│   ├── prettier.xml
│   ├── recipes.iml
│   └── watcherTasks.xml
├── .prettierignore
├── .prettierrc
├── .vscode/
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── CONTRIBUTERS.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── SECURITY.md
├── boot.sh
├── cookbook/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── connectors/
│   │   ├── __init__.py
│   │   ├── connector.py
│   │   ├── connector_manager.py
│   │   └── homeassistant.py
│   ├── fixtures/
│   │   └── german_example_tags.json
│   ├── forms.py
│   ├── helper/
│   │   ├── AllAuthCustomAdapter.py
│   │   ├── CustomStorageClass.py
│   │   ├── HelperFunctions.py
│   │   ├── __init__.py
│   │   ├── ai_helper.py
│   │   ├── automation_helper.py
│   │   ├── batch_edit_helper.py
│   │   ├── cache_helper.py
│   │   ├── context_processors.py
│   │   ├── cooklang_parser.py
│   │   ├── drf_spectacular_hooks.py
│   │   ├── fdc_helper.py
│   │   ├── image_processing.py
│   │   ├── ingredient_parser.py
│   │   ├── mdx_attributes.py
│   │   ├── mdx_urlize.py
│   │   ├── open_data_importer.py
│   │   ├── permission_config.py
│   │   ├── permission_helper.py
│   │   ├── property_helper.py
│   │   ├── recipe_search.py
│   │   ├── recipe_url_import.py
│   │   ├── scope_middleware.py
│   │   ├── shopping_helper.py
│   │   ├── template_helper.py
│   │   └── unit_conversion_helper.py
│   ├── integration/
│   │   ├── cheftap.py
│   │   ├── chowdown.py
│   │   ├── cookbookapp.py
│   │   ├── cooklang.py
│   │   ├── cookmate.py
│   │   ├── copymethat.py
│   │   ├── default.py
│   │   ├── domestica.py
│   │   ├── gourmet.py
│   │   ├── integration.py
│   │   ├── mealie.py
│   │   ├── mealie1.py
│   │   ├── mealmaster.py
│   │   ├── melarecipes.py
│   │   ├── nextcloud_cookbook.py
│   │   ├── openeats.py
│   │   ├── paprika.py
│   │   ├── pdfexport.py
│   │   ├── pepperplate.py
│   │   ├── plantoeat.py
│   │   ├── recettetek.py
│   │   ├── recipekeeper.py
│   │   ├── recipesage.py
│   │   ├── rezeptsuitede.py
│   │   ├── rezkonv.py
│   │   └── saffron.py
│   ├── locale/
│   │   ├── ar/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── bg/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ca/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── da/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── el/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── es/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── he/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hu_HU/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hy/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── id/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ko/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── lv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nb_NO/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nn/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── rn/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ro/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── tr/
│   │   │   ├── LC_MESSAGES/
│   │   │   │   ├── django.mo
│   │   │   │   └── django.po
│   │   │   └── id/
│   │   │       └── LC_MESSAGES/
│   │   │           └── django.po
│   │   ├── uk/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── vi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── zh_CN/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   └── zh_Hant/
│   │       └── LC_MESSAGES/
│   │           ├── django.mo
│   │           └── django.po
│   ├── management/
│   │   └── commands/
│   │       ├── export.py
│   │       ├── fix_duplicate_properties.py
│   │       ├── import.py
│   │       ├── rebuildindex.py
│   │       └── seed_basic_data.py
│   ├── managers.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0227_space_ai_default_provider_and_more.py
│   │   ├── 0002_auto_20191119_2035.py
│   │   ├── 0003_enable_pgtrm.py
│   │   ├── 0004_storage_created_by.py
│   │   ├── 0005_recipebook_recipebookentry.py
│   │   ├── 0006_recipe_image.py
│   │   ├── 0007_auto_20191226_0852.py
│   │   ├── 0008_mealplan.py
│   │   ├── 0009_auto_20200130_1056.py
│   │   ├── 0010_auto_20200130_1059.py
│   │   ├── 0011_remove_recipeingredients_unit.py
│   │   ├── 0012_auto_20200130_1116.py
│   │   ├── 0013_userpreference.py
│   │   ├── 0014_auto_20200213_2332.py
│   │   ├── 0015_auto_20200213_2334.py
│   │   ├── 0016_auto_20200213_2335.py
│   │   ├── 0017_auto_20200216_2257.py
│   │   ├── 0018_auto_20200216_2303.py
│   │   ├── 0019_ingredient.py
│   │   ├── 0020_recipeingredient_ingredient.py
│   │   ├── 0021_auto_20200216_2309.py
│   │   ├── 0022_remove_recipeingredient_name.py
│   │   ├── 0023_auto_20200216_2311.py
│   │   ├── 0024_auto_20200216_2313.py
│   │   ├── 0025_userpreference_nav_color.py
│   │   ├── 0026_auto_20200219_1605.py
│   │   ├── 0027_ingredient_recipe.py
│   │   ├── 0028_auto_20200317_1901.py
│   │   ├── 0029_auto_20200317_1901.py
│   │   ├── 0030_recipeingredient_note.py
│   │   ├── 0031_auto_20200407_1841.py
│   │   ├── 0032_userpreference_default_unit.py
│   │   ├── 0033_userpreference_default_page.py
│   │   ├── 0034_auto_20200426_1614.py
│   │   ├── 0035_auto_20200427_1637.py
│   │   ├── 0036_auto_20200427_1800.py
│   │   ├── 0037_userpreference_search_style.py
│   │   ├── 0038_auto_20200502_1259.py
│   │   ├── 0039_recipebook_shared.py
│   │   ├── 0040_auto_20200502_1433.py
│   │   ├── 0041_auto_20200502_1446.py
│   │   ├── 0042_cooklog.py
│   │   ├── 0043_auto_20200507_2302.py
│   │   ├── 0044_viewlog.py
│   │   ├── 0045_userpreference_show_recent.py
│   │   ├── 0046_auto_20200602_1133.py
│   │   ├── 0047_auto_20200602_1133.py
│   │   ├── 0048_auto_20200602_1140.py
│   │   ├── 0049_mealtype_created_by.py
│   │   ├── 0050_auto_20200611_1509.py
│   │   ├── 0051_auto_20200611_1518.py
│   │   ├── 0052_userpreference_ingredient_decimals.py
│   │   ├── 0053_auto_20200611_2217.py
│   │   ├── 0054_sharelink.py
│   │   ├── 0055_auto_20200616_1236.py
│   │   ├── 0056_auto_20200625_2118.py
│   │   ├── 0056_auto_20200625_2157.py
│   │   ├── 0057_auto_20200625_2127.py
│   │   ├── 0058_auto_20200625_2128.py
│   │   ├── 0059_auto_20200625_2137.py
│   │   ├── 0060_auto_20200625_2144.py
│   │   ├── 0061_merge_20200625_2209.py
│   │   ├── 0062_auto_20200625_2219.py
│   │   ├── 0063_auto_20200625_2230.py
│   │   ├── 0064_auto_20200625_2329.py
│   │   ├── 0065_auto_20200626_1444.py
│   │   ├── 0066_auto_20200626_1455.py
│   │   ├── 0067_auto_20200629_1508.py
│   │   ├── 0068_auto_20200629_2127.py
│   │   ├── 0069_auto_20200629_2134.py
│   │   ├── 0070_auto_20200701_2007.py
│   │   ├── 0071_auto_20200701_2048.py
│   │   ├── 0072_step_show_as_header.py
│   │   ├── 0073_auto_20200708_2311.py
│   │   ├── 0074_remove_keyword_created_by.py
│   │   ├── 0075_shoppinglist_shoppinglistentry_shoppinglistrecipe.py
│   │   ├── 0076_shoppinglist_entries.py
│   │   ├── 0077_invitelink.py
│   │   ├── 0078_invitelink_used_by.py
│   │   ├── 0079_invitelink_group.py
│   │   ├── 0080_auto_20200921_2331.py
│   │   ├── 0081_auto_20200921_2349.py
│   │   ├── 0082_auto_20200922_1143.py
│   │   ├── 0083_space.py
│   │   ├── 0084_auto_20200922_1233.py
│   │   ├── 0085_auto_20200922_1235.py
│   │   ├── 0086_auto_20200929_1143.py
│   │   ├── 0087_auto_20200929_1152.py
│   │   ├── 0088_shoppinglist_finished.py
│   │   ├── 0089_auto_20201117_2222.py
│   │   ├── 0090_auto_20201214_1359.py
│   │   ├── 0091_auto_20201226_1551.py
│   │   ├── 0092_recipe_servings.py
│   │   ├── 0093_auto_20201231_1236.py
│   │   ├── 0094_auto_20201231_1238.py
│   │   ├── 0095_auto_20210107_1804.py
│   │   ├── 0096_auto_20210109_2044.py
│   │   ├── 0097_auto_20210113_1315.py
│   │   ├── 0098_auto_20210113_1320.py
│   │   ├── 0099_auto_20210113_1518.py
│   │   ├── 0100_recipe_servings_text.py
│   │   ├── 0101_storage_path.py
│   │   ├── 0102_auto_20210125_1147.py
│   │   ├── 0103_food_ignore_shopping.py
│   │   ├── 0104_auto_20210125_2133.py
│   │   ├── 0105_auto_20210126_1604.py
│   │   ├── 0106_shoppinglist_supermarket.py
│   │   ├── 0107_auto_20210128_1535.py
│   │   ├── 0108_auto_20210219_1410.py
│   │   ├── 0109_auto_20210221_1204.py
│   │   ├── 0110_auto_20210221_1406.py
│   │   ├── 0111_space_created_by.py
│   │   ├── 0112_remove_synclog_space.py
│   │   ├── 0113_auto_20210317_2017.py
│   │   ├── 0114_importlog.py
│   │   ├── 0115_telegrambot.py
│   │   ├── 0116_auto_20210319_0012.py
│   │   ├── 0117_space_max_recipes.py
│   │   ├── 0118_auto_20210406_1805.py
│   │   ├── 0119_auto_20210411_2101.py
│   │   ├── 0120_bookmarklet.py
│   │   ├── 0121_auto_20210518_1638.py
│   │   ├── 0122_auto_20210527_1712.py
│   │   ├── 0123_invitelink_email.py
│   │   ├── 0124_alter_userpreference_theme.py
│   │   ├── 0125_space_demo.py
│   │   ├── 0126_alter_userpreference_theme.py
│   │   ├── 0127_remove_invitelink_username.py
│   │   ├── 0128_userfile.py
│   │   ├── 0129_auto_20210608_1233.py
│   │   ├── 0130_alter_userfile_file_size_kb.py
│   │   ├── 0131_auto_20210608_1929.py
│   │   ├── 0132_sharelink_request_count.py
│   │   ├── 0133_sharelink_abuse_blocked.py
│   │   ├── 0134_space_allow_sharing.py
│   │   ├── 0135_auto_20210615_2210.py
│   │   ├── 0136_auto_20210617_1343.py
│   │   ├── 0137_auto_20210617_1501.py
│   │   ├── 0138_auto_20210617_1602.py
│   │   ├── 0139_space_created_at.py
│   │   ├── 0140_userpreference_created_at.py
│   │   ├── 0141_auto_20210713_1042.py
│   │   ├── 0142_alter_userpreference_search_style.py
│   │   ├── 0143_build_full_text_index.py
│   │   ├── 0144_create_searchfields.py
│   │   ├── 0145_alter_userpreference_search_style.py
│   │   ├── 0146_alter_userpreference_use_fractions.py
│   │   ├── 0147_keyword_to_tree.py
│   │   ├── 0148_auto_20210813_1829.py
│   │   ├── 0149_fix_leading_trailing_spaces.py
│   │   ├── 0150_food_to_tree.py
│   │   ├── 0151_auto_20210915_1037.py
│   │   ├── 0152_automation.py
│   │   ├── 0153_auto_20210915_2327.py
│   │   ├── 0154_auto_20210922_1705.py
│   │   ├── 0155_mealtype_default.py
│   │   ├── 0156_searchpreference_trigram_threshold.py
│   │   ├── 0157_alter_searchpreference_trigram.py
│   │   ├── 0158_userpreference_use_kj.py
│   │   ├── 0159_add_shoppinglistentry_fields.py
│   │   ├── 0160_delete_shoppinglist_orphans.py
│   │   ├── 0161_alter_shoppinglistentry_food.py
│   │   ├── 0162_userpreference_csv_delim.py
│   │   ├── 0163_auto_20220105_0758.py
│   │   ├── 0164_space_show_facet_count.py
│   │   ├── 0165_remove_step_type.py
│   │   ├── 0166_alter_userpreference_shopping_add_onhand.py
│   │   ├── 0167_userpreference_left_handed.py
│   │   ├── 0168_add_unit_searchfields.py
│   │   ├── 0169_exportlog.py
│   │   ├── 0170_auto_20220207_1848.py
│   │   ├── 0171_alter_searchpreference_trigram_threshold.py
│   │   ├── 0172_ingredient_original_text.py
│   │   ├── 0173_recipe_source_url.py
│   │   ├── 0174_alter_food_substitute_userspace.py
│   │   ├── 0175_remove_userpreference_space.py
│   │   ├── 0176_alter_searchpreference_icontains_and_more.py
│   │   ├── 0177_recipe_show_ingredient_overview.py
│   │   ├── 0178_remove_userpreference_search_style_and_more.py
│   │   ├── 0179_recipe_private_recipe_shared.py
│   │   ├── 0180_invitelink_reusable.py
│   │   ├── 0181_space_image.py
│   │   ├── 0182_userpreference_image.py
│   │   ├── 0183_alter_space_image.py
│   │   ├── 0184_alter_userpreference_image.py
│   │   ├── 0185_food_plural_name_ingredient_always_use_plural_food_and_more.py
│   │   ├── 0186_automation_order_alter_automation_type.py
│   │   ├── 0187_alter_space_use_plural.py
│   │   ├── 0188_space_no_sharing_limit.py
│   │   ├── 0189_property_propertytype_unitconversion_food_fdc_id_and_more.py
│   │   ├── 0190_auto_20230525_1506.py
│   │   ├── 0191_foodproperty_property_import_food_id_and_more.py
│   │   ├── 0192_food_food_unique_open_data_slug_per_space_and_more.py
│   │   ├── 0193_space_internal_note.py
│   │   ├── 0194_alter_food_properties_food_amount.py
│   │   ├── 0195_invitelink_internal_note_userspace_internal_note_and_more.py
│   │   ├── 0196_food_url.py
│   │   ├── 0197_step_show_ingredients_table_and_more.py
│   │   ├── 0198_propertytype_order.py
│   │   ├── 0199_alter_propertytype_options_alter_automation_type_and_more.py
│   │   ├── 0200_alter_propertytype_options_remove_keyword_icon_and_more.py
│   │   ├── 0201_rename_date_mealplan_from_date_mealplan_to_date.py
│   │   ├── 0202_remove_space_show_facet_count.py
│   │   ├── 0203_alter_unique_contstraints.py
│   │   ├── 0204_propertytype_fdc_id.py
│   │   ├── 0205_alter_food_fdc_id_alter_propertytype_fdc_id.py
│   │   ├── 0206_rename_sticky_navbar_userpreference_nav_sticky_and_more.py
│   │   ├── 0207_space_logo_color_128_space_logo_color_144_and_more.py
│   │   ├── 0208_space_app_name_userpreference_max_owned_spaces.py
│   │   ├── 0209_remove_space_use_plural.py
│   │   ├── 0210_shoppinglistentry_updated_at.py
│   │   ├── 0211_recipebook_order.py
│   │   ├── 0212_alter_property_property_amount.py
│   │   ├── 0213_remove_property_property_unique_import_food_per_space_and_more.py
│   │   ├── 0214_cooklog_comment_cooklog_updated_at_and_more.py
│   │   ├── 0215_connectorconfig.py
│   │   ├── 0216_delete_shoppinglist.py
│   │   ├── 0217_alter_userpreference_default_page.py
│   │   ├── 0218_alter_mealplan_from_date_alter_mealplan_to_date.py
│   │   ├── 0219_connectorconfig_supports_description_field.py
│   │   ├── 0220_shoppinglistrecipe_created_by_and_more.py
│   │   ├── 0221_migrate_shoppinglistrecipe_space_created_by.py
│   │   ├── 0222_alter_shoppinglistrecipe_created_by_and_more.py
│   │   ├── 0223_auto_20250831_1111.py
│   │   ├── 0224_space_ai_credits_balance_space_ai_credits_monthly_and_more.py
│   │   ├── 0225_space_ai_enabled.py
│   │   ├── 0226_aiprovider_log_credit_cost_and_more.py
│   │   ├── 0227_space_ai_default_provider_and_more.py
│   │   ├── 0228_space_space_setup_completed.py
│   │   ├── 0229_alter_ailog_options_alter_aiprovider_options_and_more.py
│   │   ├── 0230_auto_20250925_2056.py
│   │   ├── 0231_alter_aiprovider_options_alter_automation_options_and_more.py
│   │   ├── 0232_shoppinglist.py
│   │   ├── 0233_food_shopping_lists_shoppinglistentry_shopping_lists_and_more.py
│   │   ├── 0234_alter_shoppinglist_options_and_more.py
│   │   ├── 0235_recipe_diameter_recipe_diameter_text.py
│   │   ├── 0236_household_userspace_household_inventorylocation_and_more.py
│   │   ├── 0237_remove_mealtype_mt_unique_name_per_space_and_more.py
│   │   ├── 0238_auto_20260312_1920.py
│   │   └── __init__.py
│   ├── models.py
│   ├── provider/
│   │   ├── __init__.py
│   │   ├── dropbox.py
│   │   ├── local.py
│   │   ├── nextcloud.py
│   │   └── provider.py
│   ├── serializer.py
│   ├── signals.py
│   ├── static/
│   │   ├── custom/
│   │   │   └── css/
│   │   │       └── markdown_blockquote.css
│   │   └── pdfjs/
│   │       ├── LICENSE
│   │       └── web/
│   │           ├── cmaps/
│   │           │   ├── 78-EUC-H.bcmap
│   │           │   ├── 78-EUC-V.bcmap
│   │           │   ├── 78-H.bcmap
│   │           │   ├── 78-RKSJ-H.bcmap
│   │           │   ├── 78-RKSJ-V.bcmap
│   │           │   ├── 78-V.bcmap
│   │           │   ├── 78ms-RKSJ-H.bcmap
│   │           │   ├── 78ms-RKSJ-V.bcmap
│   │           │   ├── 83pv-RKSJ-H.bcmap
│   │           │   ├── 90ms-RKSJ-H.bcmap
│   │           │   ├── 90ms-RKSJ-V.bcmap
│   │           │   ├── 90msp-RKSJ-H.bcmap
│   │           │   ├── 90msp-RKSJ-V.bcmap
│   │           │   ├── 90pv-RKSJ-H.bcmap
│   │           │   ├── 90pv-RKSJ-V.bcmap
│   │           │   ├── Add-H.bcmap
│   │           │   ├── Add-RKSJ-H.bcmap
│   │           │   ├── Add-RKSJ-V.bcmap
│   │           │   ├── Add-V.bcmap
│   │           │   ├── Adobe-CNS1-0.bcmap
│   │           │   ├── Adobe-CNS1-1.bcmap
│   │           │   ├── Adobe-CNS1-2.bcmap
│   │           │   ├── Adobe-CNS1-3.bcmap
│   │           │   ├── Adobe-CNS1-4.bcmap
│   │           │   ├── Adobe-CNS1-5.bcmap
│   │           │   ├── Adobe-CNS1-6.bcmap
│   │           │   ├── Adobe-CNS1-UCS2.bcmap
│   │           │   ├── Adobe-GB1-0.bcmap
│   │           │   ├── Adobe-GB1-1.bcmap
│   │           │   ├── Adobe-GB1-2.bcmap
│   │           │   ├── Adobe-GB1-3.bcmap
│   │           │   ├── Adobe-GB1-4.bcmap
│   │           │   ├── Adobe-GB1-5.bcmap
│   │           │   ├── Adobe-GB1-UCS2.bcmap
│   │           │   ├── Adobe-Japan1-0.bcmap
│   │           │   ├── Adobe-Japan1-1.bcmap
│   │           │   ├── Adobe-Japan1-2.bcmap
│   │           │   ├── Adobe-Japan1-3.bcmap
│   │           │   ├── Adobe-Japan1-4.bcmap
│   │           │   ├── Adobe-Japan1-5.bcmap
│   │           │   ├── Adobe-Japan1-6.bcmap
│   │           │   ├── Adobe-Japan1-UCS2.bcmap
│   │           │   ├── Adobe-Korea1-0.bcmap
│   │           │   ├── Adobe-Korea1-1.bcmap
│   │           │   ├── Adobe-Korea1-2.bcmap
│   │           │   ├── Adobe-Korea1-UCS2.bcmap
│   │           │   ├── B5-H.bcmap
│   │           │   ├── B5-V.bcmap
│   │           │   ├── B5pc-H.bcmap
│   │           │   ├── B5pc-V.bcmap
│   │           │   ├── CNS-EUC-H.bcmap
│   │           │   ├── CNS-EUC-V.bcmap
│   │           │   ├── CNS1-H.bcmap
│   │           │   ├── CNS1-V.bcmap
│   │           │   ├── CNS2-H.bcmap
│   │           │   ├── CNS2-V.bcmap
│   │           │   ├── ETHK-B5-H.bcmap
│   │           │   ├── ETHK-B5-V.bcmap
│   │           │   ├── ETen-B5-H.bcmap
│   │           │   ├── ETen-B5-V.bcmap
│   │           │   ├── ETenms-B5-H.bcmap
│   │           │   ├── ETenms-B5-V.bcmap
│   │           │   ├── EUC-H.bcmap
│   │           │   ├── EUC-V.bcmap
│   │           │   ├── Ext-H.bcmap
│   │           │   ├── Ext-RKSJ-H.bcmap
│   │           │   ├── Ext-RKSJ-V.bcmap
│   │           │   ├── Ext-V.bcmap
│   │           │   ├── GB-EUC-H.bcmap
│   │           │   ├── GB-EUC-V.bcmap
│   │           │   ├── GB-H.bcmap
│   │           │   ├── GB-V.bcmap
│   │           │   ├── GBK-EUC-H.bcmap
│   │           │   ├── GBK-EUC-V.bcmap
│   │           │   ├── GBK2K-H.bcmap
│   │           │   ├── GBK2K-V.bcmap
│   │           │   ├── GBKp-EUC-H.bcmap
│   │           │   ├── GBKp-EUC-V.bcmap
│   │           │   ├── GBT-EUC-H.bcmap
│   │           │   ├── GBT-EUC-V.bcmap
│   │           │   ├── GBT-H.bcmap
│   │           │   ├── GBT-V.bcmap
│   │           │   ├── GBTpc-EUC-H.bcmap
│   │           │   ├── GBTpc-EUC-V.bcmap
│   │           │   ├── GBpc-EUC-H.bcmap
│   │           │   ├── GBpc-EUC-V.bcmap
│   │           │   ├── H.bcmap
│   │           │   ├── HKdla-B5-H.bcmap
│   │           │   ├── HKdla-B5-V.bcmap
│   │           │   ├── HKdlb-B5-H.bcmap
│   │           │   ├── HKdlb-B5-V.bcmap
│   │           │   ├── HKgccs-B5-H.bcmap
│   │           │   ├── HKgccs-B5-V.bcmap
│   │           │   ├── HKm314-B5-H.bcmap
│   │           │   ├── HKm314-B5-V.bcmap
│   │           │   ├── HKm471-B5-H.bcmap
│   │           │   ├── HKm471-B5-V.bcmap
│   │           │   ├── HKscs-B5-H.bcmap
│   │           │   ├── HKscs-B5-V.bcmap
│   │           │   ├── Hankaku.bcmap
│   │           │   ├── Hiragana.bcmap
│   │           │   ├── KSC-EUC-H.bcmap
│   │           │   ├── KSC-EUC-V.bcmap
│   │           │   ├── KSC-H.bcmap
│   │           │   ├── KSC-Johab-H.bcmap
│   │           │   ├── KSC-Johab-V.bcmap
│   │           │   ├── KSC-V.bcmap
│   │           │   ├── KSCms-UHC-H.bcmap
│   │           │   ├── KSCms-UHC-HW-H.bcmap
│   │           │   ├── KSCms-UHC-HW-V.bcmap
│   │           │   ├── KSCms-UHC-V.bcmap
│   │           │   ├── KSCpc-EUC-H.bcmap
│   │           │   ├── KSCpc-EUC-V.bcmap
│   │           │   ├── Katakana.bcmap
│   │           │   ├── LICENSE
│   │           │   ├── NWP-H.bcmap
│   │           │   ├── NWP-V.bcmap
│   │           │   ├── RKSJ-H.bcmap
│   │           │   ├── RKSJ-V.bcmap
│   │           │   ├── Roman.bcmap
│   │           │   ├── UniCNS-UCS2-H.bcmap
│   │           │   ├── UniCNS-UCS2-V.bcmap
│   │           │   ├── UniCNS-UTF16-H.bcmap
│   │           │   ├── UniCNS-UTF16-V.bcmap
│   │           │   ├── UniCNS-UTF32-H.bcmap
│   │           │   ├── UniCNS-UTF32-V.bcmap
│   │           │   ├── UniCNS-UTF8-H.bcmap
│   │           │   ├── UniCNS-UTF8-V.bcmap
│   │           │   ├── UniGB-UCS2-H.bcmap
│   │           │   ├── UniGB-UCS2-V.bcmap
│   │           │   ├── UniGB-UTF16-H.bcmap
│   │           │   ├── UniGB-UTF16-V.bcmap
│   │           │   ├── UniGB-UTF32-H.bcmap
│   │           │   ├── UniGB-UTF32-V.bcmap
│   │           │   ├── UniGB-UTF8-H.bcmap
│   │           │   ├── UniGB-UTF8-V.bcmap
│   │           │   ├── UniJIS-UCS2-H.bcmap
│   │           │   ├── UniJIS-UCS2-HW-H.bcmap
│   │           │   ├── UniJIS-UCS2-HW-V.bcmap
│   │           │   ├── UniJIS-UCS2-V.bcmap
│   │           │   ├── UniJIS-UTF16-H.bcmap
│   │           │   ├── UniJIS-UTF16-V.bcmap
│   │           │   ├── UniJIS-UTF32-H.bcmap
│   │           │   ├── UniJIS-UTF32-V.bcmap
│   │           │   ├── UniJIS-UTF8-H.bcmap
│   │           │   ├── UniJIS-UTF8-V.bcmap
│   │           │   ├── UniJIS2004-UTF16-H.bcmap
│   │           │   ├── UniJIS2004-UTF16-V.bcmap
│   │           │   ├── UniJIS2004-UTF32-H.bcmap
│   │           │   ├── UniJIS2004-UTF32-V.bcmap
│   │           │   ├── UniJIS2004-UTF8-H.bcmap
│   │           │   ├── UniJIS2004-UTF8-V.bcmap
│   │           │   ├── UniJISPro-UCS2-HW-V.bcmap
│   │           │   ├── UniJISPro-UCS2-V.bcmap
│   │           │   ├── UniJISPro-UTF8-V.bcmap
│   │           │   ├── UniJISX0213-UTF32-H.bcmap
│   │           │   ├── UniJISX0213-UTF32-V.bcmap
│   │           │   ├── UniJISX02132004-UTF32-H.bcmap
│   │           │   ├── UniJISX02132004-UTF32-V.bcmap
│   │           │   ├── UniKS-UCS2-H.bcmap
│   │           │   ├── UniKS-UCS2-V.bcmap
│   │           │   ├── UniKS-UTF16-H.bcmap
│   │           │   ├── UniKS-UTF16-V.bcmap
│   │           │   ├── UniKS-UTF32-H.bcmap
│   │           │   ├── UniKS-UTF32-V.bcmap
│   │           │   ├── UniKS-UTF8-H.bcmap
│   │           │   ├── UniKS-UTF8-V.bcmap
│   │           │   ├── V.bcmap
│   │           │   └── WP-Symbol.bcmap
│   │           ├── debugger.css
│   │           ├── debugger.mjs
│   │           ├── iccs/
│   │           │   ├── CGATS001Compat-v2-micro.icc
│   │           │   └── LICENSE
│   │           ├── locale/
│   │           │   ├── ach/
│   │           │   │   └── viewer.ftl
│   │           │   ├── af/
│   │           │   │   └── viewer.ftl
│   │           │   ├── an/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ar/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ast/
│   │           │   │   └── viewer.ftl
│   │           │   ├── az/
│   │           │   │   └── viewer.ftl
│   │           │   ├── be/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── br/
│   │           │   │   └── viewer.ftl
│   │           │   ├── brx/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ca/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cak/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ckb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cy/
│   │           │   │   └── viewer.ftl
│   │           │   ├── da/
│   │           │   │   └── viewer.ftl
│   │           │   ├── de/
│   │           │   │   └── viewer.ftl
│   │           │   ├── dsb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── el/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-CA/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-GB/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-US/
│   │           │   │   └── viewer.ftl
│   │           │   ├── eo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-AR/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-CL/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-ES/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-MX/
│   │           │   │   └── viewer.ftl
│   │           │   ├── et/
│   │           │   │   └── viewer.ftl
│   │           │   ├── eu/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fa/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ff/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fi/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fur/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fy-NL/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ga-IE/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gd/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gu-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── he/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hi-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hsb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hu/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hy-AM/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hye/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ia/
│   │           │   │   └── viewer.ftl
│   │           │   ├── id/
│   │           │   │   └── viewer.ftl
│   │           │   ├── is/
│   │           │   │   └── viewer.ftl
│   │           │   ├── it/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ja/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ka/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kab/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── km/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ko/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lij/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── locale.json
│   │           │   ├── lt/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ltg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lv/
│   │           │   │   └── viewer.ftl
│   │           │   ├── meh/
│   │           │   │   └── viewer.ftl
│   │           │   ├── mk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ml/
│   │           │   │   └── viewer.ftl
│   │           │   ├── mr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ms/
│   │           │   │   └── viewer.ftl
│   │           │   ├── my/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nb-NO/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ne-NP/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nn-NO/
│   │           │   │   └── viewer.ftl
│   │           │   ├── oc/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pa-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pt-BR/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pt-PT/
│   │           │   │   └── viewer.ftl
│   │           │   ├── rm/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ro/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ru/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sat/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sc/
│   │           │   │   └── viewer.ftl
│   │           │   ├── scn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sco/
│   │           │   │   └── viewer.ftl
│   │           │   ├── si/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── skr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── son/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sq/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sv-SE/
│   │           │   │   └── viewer.ftl
│   │           │   ├── szl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ta/
│   │           │   │   └── viewer.ftl
│   │           │   ├── te/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── th/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── trs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── uk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ur/
│   │           │   │   └── viewer.ftl
│   │           │   ├── uz/
│   │           │   │   └── viewer.ftl
│   │           │   ├── vi/
│   │           │   │   └── viewer.ftl
│   │           │   ├── wo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── xh/
│   │           │   │   └── viewer.ftl
│   │           │   ├── zh-CN/
│   │           │   │   └── viewer.ftl
│   │           │   └── zh-TW/
│   │           │       └── viewer.ftl
│   │           ├── pdf.mjs
│   │           ├── pdf.sandbox.mjs
│   │           ├── pdf.worker.mjs
│   │           ├── standard_fonts/
│   │           │   ├── FoxitDingbats.pfb
│   │           │   ├── FoxitFixed.pfb
│   │           │   ├── FoxitFixedBold.pfb
│   │           │   ├── FoxitFixedBoldItalic.pfb
│   │           │   ├── FoxitFixedItalic.pfb
│   │           │   ├── FoxitSerif.pfb
│   │           │   ├── FoxitSerifBold.pfb
│   │           │   ├── FoxitSerifBoldItalic.pfb
│   │           │   ├── FoxitSerifItalic.pfb
│   │           │   ├── FoxitSymbol.pfb
│   │           │   ├── LICENSE_FOXIT
│   │           │   └── LICENSE_LIBERATION
│   │           ├── viewer.css
│   │           ├── viewer.html
│   │           ├── viewer.mjs
│   │           └── wasm/
│   │               ├── LICENSE_OPENJPEG
│   │               ├── LICENSE_PDFJS_OPENJPEG
│   │               ├── LICENSE_PDFJS_QCMS
│   │               ├── LICENSE_QCMS
│   │               ├── openjpeg.wasm
│   │               ├── openjpeg_nowasm_fallback.js
│   │               └── qcms_bg.wasm
│   ├── templates/
│   │   ├── 404.html
│   │   ├── account/
│   │   │   ├── email.html
│   │   │   ├── email_confirm.html
│   │   │   ├── login.html
│   │   │   ├── logout.html
│   │   │   ├── password_change.html
│   │   │   ├── password_reset.html
│   │   │   ├── password_reset_done.html
│   │   │   ├── password_reset_from_key.html
│   │   │   ├── password_reset_from_key_done.html
│   │   │   ├── password_set.html
│   │   │   ├── signup.html
│   │   │   └── signup_closed.html
│   │   ├── base.html
│   │   ├── frontend/
│   │   │   └── tandoor.html
│   │   ├── index.html
│   │   ├── manifest.json
│   │   ├── markdown_info.html
│   │   ├── no_groups_info.html
│   │   ├── no_perm_info.html
│   │   ├── offline.html
│   │   ├── openid/
│   │   │   └── login.html
│   │   ├── pdf_viewer.html
│   │   ├── rest_framework/
│   │   │   └── api.html
│   │   ├── search_info.html
│   │   ├── setup.html
│   │   ├── socialaccount/
│   │   │   ├── authentication_error.html
│   │   │   ├── connections.html
│   │   │   ├── login.html
│   │   │   ├── signup.html
│   │   │   └── snippets/
│   │   │       └── provider_list.html
│   │   ├── space_overview.html
│   │   ├── system.html
│   │   ├── test.html
│   │   └── test2.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── custom_tags.py
│   │   └── theming_tags.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── docs/
│   │   │   │   └── reports/
│   │   │   │       └── tests/
│   │   │   │           ├── assets/
│   │   │   │           │   └── style.css
│   │   │   │           ├── pytest.xml
│   │   │   │           └── tests.html
│   │   │   ├── test_api_access_token.py
│   │   │   ├── test_api_ai_provider.py
│   │   │   ├── test_api_ai_timeout.py
│   │   │   ├── test_api_connector_config.py
│   │   │   ├── test_api_cook_log.py
│   │   │   ├── test_api_food.py
│   │   │   ├── test_api_food_shopping.py
│   │   │   ├── test_api_import_log.py
│   │   │   ├── test_api_ingredient.py
│   │   │   ├── test_api_invitelinke.py
│   │   │   ├── test_api_keyword.py
│   │   │   ├── test_api_meal_plan.py
│   │   │   ├── test_api_meal_type.py
│   │   │   ├── test_api_property.py
│   │   │   ├── test_api_property_type.py
│   │   │   ├── test_api_recipe.py
│   │   │   ├── test_api_recipe_book.py
│   │   │   ├── test_api_recipe_book_entry.py
│   │   │   ├── test_api_related_recipe.py
│   │   │   ├── test_api_share_link.py
│   │   │   ├── test_api_shopping_list_entryv2.py
│   │   │   ├── test_api_shopping_list_recipe.py
│   │   │   ├── test_api_shopping_recipe.py
│   │   │   ├── test_api_space.py
│   │   │   ├── test_api_step.py
│   │   │   ├── test_api_storage.py
│   │   │   ├── test_api_supermarket.py
│   │   │   ├── test_api_sync.py
│   │   │   ├── test_api_sync_log.py
│   │   │   ├── test_api_unit.py
│   │   │   ├── test_api_unit_conversion.py
│   │   │   ├── test_api_user.py
│   │   │   ├── test_api_userpreference.py
│   │   │   ├── test_api_userspace.py
│   │   │   └── test_api_view_log.py
│   │   ├── conftest.py
│   │   ├── docs/
│   │   │   └── reports/
│   │   │       └── tests/
│   │   │           ├── assets/
│   │   │           │   └── style.css
│   │   │           ├── pytest.xml
│   │   │           └── tests.html
│   │   ├── factories/
│   │   │   └── __init__.py
│   │   ├── other/
│   │   │   ├── __init__.py
│   │   │   ├── _recipes.py
│   │   │   ├── docs/
│   │   │   │   └── reports/
│   │   │   │       └── tests/
│   │   │   │           └── assets/
│   │   │   │               └── style.css
│   │   │   ├── test_automations.py
│   │   │   ├── test_connector_manager.py
│   │   │   ├── test_cooklang_integration.py
│   │   │   ├── test_data/
│   │   │   │   ├── Cooklang/
│   │   │   │   │   ├── American Pancakes.cook
│   │   │   │   │   ├── Another Lemon Blueberry Bread.cook
│   │   │   │   │   └── Butter Swirl Shortbread Cookies.cook
│   │   │   │   ├── allrecipes.html
│   │   │   │   ├── americastestkitchen.html
│   │   │   │   ├── chefkoch.html
│   │   │   │   ├── chefkoch2.html
│   │   │   │   ├── cookpad.html
│   │   │   │   ├── cookscountry.html
│   │   │   │   ├── delish.html
│   │   │   │   ├── foodnetwork.html
│   │   │   │   ├── giallozafferano.html
│   │   │   │   ├── journaldesfemmes.html
│   │   │   │   ├── madamedessert.html
│   │   │   │   ├── madamedessert.json
│   │   │   │   ├── marmiton.html
│   │   │   │   ├── regex_recipe.html
│   │   │   │   ├── tasteofhome.html
│   │   │   │   ├── thespruceeats.html
│   │   │   │   └── tudogostoso.html
│   │   │   ├── test_food_property.py
│   │   │   ├── test_ingredient_editor_performance.py
│   │   │   ├── test_ingredient_parser.py
│   │   │   ├── test_local_provider.py
│   │   │   ├── test_makenow_filter.py
│   │   │   ├── test_nested_serializer.py
│   │   │   ├── test_permission_helper.py
│   │   │   ├── test_recipe_full_text_search.py
│   │   │   ├── test_schemas.py
│   │   │   ├── test_social_auth.py
│   │   │   ├── test_theming.py
│   │   │   ├── test_unit_conversion.py
│   │   │   └── test_url_import.py
│   │   ├── resources/
│   │   │   └── websites/
│   │   │       ├── ld_json_1.html
│   │   │       ├── ld_json_2.html
│   │   │       ├── ld_json_3.html
│   │   │       ├── ld_json_4.html
│   │   │       ├── ld_json_itemList.html
│   │   │       ├── ld_json_multiple.html
│   │   │       ├── micro_data_1.html
│   │   │       ├── micro_data_2.html
│   │   │       ├── micro_data_3.html
│   │   │       └── micro_data_4.html
│   │   └── views/
│   │       ├── __init__.py
│   │       ├── test_views_api.py
│   │       └── test_views_general.py
│   ├── urls.py
│   ├── version_info.py
│   └── views/
│       ├── __init__.py
│       ├── api.py
│       ├── import_export.py
│       ├── telegram.py
│       └── views.py
├── docs/
│   ├── CNAME
│   ├── contribute/
│   │   ├── contribute.md
│   │   ├── documentation.md
│   │   ├── feature_contrib/
│   │   │   ├── Integration.md
│   │   │   └── featureguides.md
│   │   ├── guidelines.md
│   │   ├── installation.md
│   │   ├── pycharm.md
│   │   ├── related.md
│   │   ├── translations.md
│   │   └── vscode.md
│   ├── faq.md
│   ├── features/
│   │   ├── ai.md
│   │   ├── authentication.md
│   │   ├── automation.md
│   │   ├── connectors.md
│   │   ├── external_recipes.md
│   │   ├── import_export.md
│   │   ├── shopping.md
│   │   ├── telegram_bot.md
│   │   └── templating.md
│   ├── index.md
│   ├── install/
│   │   ├── archlinux.md
│   │   ├── docker/
│   │   │   ├── apache-proxy/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── ipv6_plain/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── nginx-proxy/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── plain/
│   │   │   │   └── docker-compose.yml
│   │   │   └── traefik-nginx/
│   │   │       └── docker-compose.yml
│   │   ├── docker.md
│   │   ├── helmChart.md
│   │   ├── homeassistant.md
│   │   ├── k8s/
│   │   │   ├── 10-configmap.yaml
│   │   │   ├── 15-secrets.yaml
│   │   │   ├── 20-service-account.yaml
│   │   │   ├── 30-pvc.yaml
│   │   │   ├── 40-sts-postgresql.yaml
│   │   │   ├── 45-service-db.yaml
│   │   │   ├── 50-deployment.yaml
│   │   │   ├── 60-service.yaml
│   │   │   └── 70-ingress.yaml
│   │   ├── kubernetes.md
│   │   ├── kubesail.md
│   │   ├── manual.md
│   │   ├── other.md
│   │   ├── swag.md
│   │   ├── synology.md
│   │   ├── truenas_portainer.md
│   │   ├── unraid.md
│   │   └── wsl.md
│   ├── preview.xcf
│   ├── stylesheets/
│   │   └── extra.css
│   ├── system/
│   │   ├── backup.md
│   │   ├── configuration.md
│   │   ├── migration_sqlite-postgres.md
│   │   ├── permissions.md
│   │   └── updating.md
│   └── tests/
│       ├── assets/
│       │   └── style.css
│       ├── pytest.xml
│       └── tests.html
├── generate_api_client.py
├── http.d/
│   ├── Recipes.conf.template
│   └── errorpages/
│       └── http502.html
├── make_compile_messages.py
├── manage.py
├── mkdocs.yml
├── nginx/
│   └── conf.d/
│       ├── Recipes.conf
│       └── errorpages/
│           └── http502.html
├── openapitools.json
├── plugin.py
├── pyproject.toml
├── pytest.ini
├── recipes/
│   ├── __init__.py
│   ├── locale/
│   │   ├── ar/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── bg/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ca/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── da/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── el/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── es/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── he/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hu_HU/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hy/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── id/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── lv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nb_NO/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── rn/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ro/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── tr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── uk/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── vi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── zh_CN/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   └── zh_Hant/
│   │       └── LC_MESSAGES/
│   │           ├── django.mo
│   │           └── django.po
│   ├── middleware.py
│   ├── settings.py
│   ├── test_settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements.txt
├── version.py
└── vue3/
    ├── env.d.ts
    ├── package.json
    ├── src/
    │   ├── apps/
    │   │   └── tandoor/
    │   │       ├── Tandoor.vue
    │   │       └── main.ts
    │   ├── assets/
    │   │   ├── bookmarklet_v3.js
    │   │   └── vueform.css
    │   ├── components/
    │   │   ├── buttons/
    │   │   │   ├── AiActionButton.vue
    │   │   │   └── BtnCopy.vue
    │   │   ├── dialogs/
    │   │   │   ├── AddToShoppingDialog.vue
    │   │   │   ├── AutoPlanDialog.vue
    │   │   │   ├── BatchDeleteDialog.vue
    │   │   │   ├── BatchEditFoodDialog.vue
    │   │   │   ├── BatchEditRecipeDialog.vue
    │   │   │   ├── DeleteConfirmDialog.vue
    │   │   │   ├── FdcSearchDialog.vue
    │   │   │   ├── FreezerExpiryDialog.vue
    │   │   │   ├── HelpDialog.vue
    │   │   │   ├── ImportTandoorDialog.vue
    │   │   │   ├── InventoryEntryLogDialog.vue
    │   │   │   ├── MealPlanIcalDialog.vue
    │   │   │   ├── MessageListDialog.vue
    │   │   │   ├── ModelEditDialog.vue
    │   │   │   ├── ModelMergeDialog.vue
    │   │   │   ├── RecipeScalingDialog.vue
    │   │   │   ├── RecipeShareDialog.vue
    │   │   │   ├── ShoppingExportDialog.vue
    │   │   │   ├── ShoppingLineItemDialog.vue
    │   │   │   ├── StepIngredientSorterDialog.vue
    │   │   │   ├── SyncDialog.vue
    │   │   │   └── VClosableCardTitle.vue
    │   │   ├── display/
    │   │   │   ├── BookEntryCard.vue
    │   │   │   ├── ClosableHelpAlert.vue
    │   │   │   ├── DatabaseLinkCol.vue
    │   │   │   ├── DatabaseModelCol.vue
    │   │   │   ├── ExternalRecipeViewer.vue
    │   │   │   ├── HelpView.vue
    │   │   │   ├── HorizontalMealPlanWindow.vue
    │   │   │   ├── HorizontalRecipeWindow.vue
    │   │   │   ├── ImportLogViewer.vue
    │   │   │   ├── IngredientString.vue
    │   │   │   ├── IngredientsTable.vue
    │   │   │   ├── IngredientsTableRow.vue
    │   │   │   ├── Instructions.vue
    │   │   │   ├── InventoryEntryTable.vue
    │   │   │   ├── KeywordsBar.vue
    │   │   │   ├── MealPlanCalendarHeader.vue
    │   │   │   ├── MealPlanCalendarItem.vue
    │   │   │   ├── MealPlanView.vue
    │   │   │   ├── NavigationDrawerContextMenu.vue
    │   │   │   ├── PrivateRecipeBadge.vue
    │   │   │   ├── PropertyView.vue
    │   │   │   ├── RandomIcon.vue
    │   │   │   ├── RecipeActivity.vue
    │   │   │   ├── RecipeCard.vue
    │   │   │   ├── RecipeImage.vue
    │   │   │   ├── RecipeView.vue
    │   │   │   ├── ScalableNumber.vue
    │   │   │   ├── ShoppingLineItem.vue
    │   │   │   ├── ShoppingListView.vue
    │   │   │   ├── ShoppingListsBar.vue
    │   │   │   ├── SpaceLimitsInfo.vue
    │   │   │   ├── StepView.vue
    │   │   │   ├── StepsOverview.vue
    │   │   │   ├── ThankYouNote.vue
    │   │   │   ├── Timer.vue
    │   │   │   └── VSnackbarQueued.vue
    │   │   ├── inputs/
    │   │   │   ├── BaseUnitSelect.vue
    │   │   │   ├── CategorySelectChip.vue
    │   │   │   ├── GlobalSearchDialog.vue
    │   │   │   ├── HierarchyEditor.vue
    │   │   │   ├── LanguageSelect.vue
    │   │   │   ├── ModelSelect.vue
    │   │   │   ├── ModelSelectVuetify.vue
    │   │   │   ├── NumberScalerDialog.vue
    │   │   │   ├── PropertiesEditor.vue
    │   │   │   ├── RatingField.vue
    │   │   │   ├── RecipeContextMenu.vue
    │   │   │   ├── ShoppingListEntryInput.vue
    │   │   │   ├── ShoppingListSelectChip.vue
    │   │   │   ├── StepEditor.vue
    │   │   │   ├── StepMarkdownEditor.vue
    │   │   │   └── UserFileField.vue
    │   │   ├── model_editors/
    │   │   │   ├── AccessTokenEditor.vue
    │   │   │   ├── AiProviderEditor.vue
    │   │   │   ├── AutomationEditor.vue
    │   │   │   ├── ConnectorConfigEditor.vue
    │   │   │   ├── CookLogEditor.vue
    │   │   │   ├── CustomFilterEditor.vue
    │   │   │   ├── FoodEditor.vue
    │   │   │   ├── HouseholdEditor.vue
    │   │   │   ├── InventoryLocationEditor.vue
    │   │   │   ├── InviteLinkEditor.vue
    │   │   │   ├── KeywordEditor.vue
    │   │   │   ├── MealPlanEditor.vue
    │   │   │   ├── MealTypeEditor.vue
    │   │   │   ├── ModelEditorBase.vue
    │   │   │   ├── PropertyEditor.vue
    │   │   │   ├── PropertyTypeEditor.vue
    │   │   │   ├── RecipeBookEditor.vue
    │   │   │   ├── RecipeEditor.vue
    │   │   │   ├── ShoppingListEditor.vue
    │   │   │   ├── ShoppingListEntryEditor.vue
    │   │   │   ├── SpaceEditor.vue
    │   │   │   ├── StorageEditor.vue
    │   │   │   ├── SupermarketCategoryEditor.vue
    │   │   │   ├── SupermarketEditor.vue
    │   │   │   ├── SyncEditor.vue
    │   │   │   ├── UnitConversionEditor.vue
    │   │   │   ├── UnitEditor.vue
    │   │   │   ├── UserFileEditor.vue
    │   │   │   └── UserSpaceEditor.vue
    │   │   ├── settings/
    │   │   │   ├── AccountSettings.vue
    │   │   │   ├── ApiSettings.vue
    │   │   │   ├── CosmeticSettings.vue
    │   │   │   ├── ExportDataSettings.vue
    │   │   │   ├── MealPlanDeviceSettings.vue
    │   │   │   ├── MealPlanSettings.vue
    │   │   │   ├── OpenDataImportSettings.vue
    │   │   │   ├── SearchSettings.vue
    │   │   │   ├── ShoppingSettings.vue
    │   │   │   └── SpaceSettings.vue
    │   │   └── tables/
    │   │       └── InventoryEntryLogTable.vue
    │   ├── composables/
    │   │   ├── useDjangoUrls.ts
    │   │   ├── useFileApi.ts
    │   │   ├── useModelEditorFunctions.ts
    │   │   └── useNavigation.ts
    │   ├── i18n.ts
    │   ├── locales/
    │   │   ├── ar.json
    │   │   ├── bg.json
    │   │   ├── ca.json
    │   │   ├── cs.json
    │   │   ├── da.json
    │   │   ├── de.json
    │   │   ├── el.json
    │   │   ├── en.json
    │   │   ├── es.json
    │   │   ├── fi.json
    │   │   ├── fr.json
    │   │   ├── he.json
    │   │   ├── hr.json
    │   │   ├── hu.json
    │   │   ├── hy.json
    │   │   ├── id.json
    │   │   ├── is.json
    │   │   ├── it.json
    │   │   ├── ko.json
    │   │   ├── lt.json
    │   │   ├── lv.json
    │   │   ├── nb_NO.json
    │   │   ├── nl.json
    │   │   ├── nn.json
    │   │   ├── pl.json
    │   │   ├── pt.json
    │   │   ├── pt_BR.json
    │   │   ├── ro.json
    │   │   ├── ru.json
    │   │   ├── sl.json
    │   │   ├── sv.json
    │   │   ├── tr.json
    │   │   ├── uk.json
    │   │   ├── zh_Hans.json
    │   │   └── zh_Hant.json
    │   ├── openapi/
    │   │   ├── .openapi-generator/
    │   │   │   ├── FILES
    │   │   │   └── VERSION
    │   │   ├── .openapi-generator-ignore
    │   │   ├── apis/
    │   │   │   ├── ApiApi.ts
    │   │   │   ├── ApiTokenAuthApi.ts
    │   │   │   └── index.ts
    │   │   ├── index.ts
    │   │   ├── models/
    │   │   │   ├── AccessToken.ts
    │   │   │   ├── AiLog.ts
    │   │   │   ├── AiProvider.ts
    │   │   │   ├── AlignmentEnum.ts
    │   │   │   ├── AuthToken.ts
    │   │   │   ├── AutoMealPlan.ts
    │   │   │   ├── Automation.ts
    │   │   │   ├── AutomationTypeEnum.ts
    │   │   │   ├── BaseUnitEnum.ts
    │   │   │   ├── BookingTypeEnum.ts
    │   │   │   ├── BookmarkletImport.ts
    │   │   │   ├── BookmarkletImportList.ts
    │   │   │   ├── ConnectorConfig.ts
    │   │   │   ├── ConnectorConfigConfig.ts
    │   │   │   ├── ConnectorConfigTypeEnum.ts
    │   │   │   ├── CookLog.ts
    │   │   │   ├── CustomFilter.ts
    │   │   │   ├── DefaultPageEnum.ts
    │   │   │   ├── DeleteEnum.ts
    │   │   │   ├── EnterpriseKeyword.ts
    │   │   │   ├── EnterpriseSocialEmbed.ts
    │   │   │   ├── EnterpriseSocialEmbedTypeEnum.ts
    │   │   │   ├── EnterpriseSocialRecipeSearch.ts
    │   │   │   ├── EnterpriseSpace.ts
    │   │   │   ├── ExportLog.ts
    │   │   │   ├── ExportRequest.ts
    │   │   │   ├── FdcQuery.ts
    │   │   │   ├── FdcQueryFoods.ts
    │   │   │   ├── Food.ts
    │   │   │   ├── FoodBatchUpdate.ts
    │   │   │   ├── FoodInheritField.ts
    │   │   │   ├── FoodShopping.ts
    │   │   │   ├── FoodShoppingUpdate.ts
    │   │   │   ├── FoodSimple.ts
    │   │   │   ├── GenericModel.ts
    │   │   │   ├── GenericModelReference.ts
    │   │   │   ├── Group.ts
    │   │   │   ├── Household.ts
    │   │   │   ├── ImportImage.ts
    │   │   │   ├── ImportLog.ts
    │   │   │   ├── ImportOpenData.ts
    │   │   │   ├── ImportOpenDataMetaData.ts
    │   │   │   ├── ImportOpenDataResponse.ts
    │   │   │   ├── ImportOpenDataResponseDetail.ts
    │   │   │   ├── ImportOpenDataVersionMetaData.ts
    │   │   │   ├── Ingredient.ts
    │   │   │   ├── IngredientParserRequest.ts
    │   │   │   ├── IngredientParserResponse.ts
    │   │   │   ├── IngredientSimple.ts
    │   │   │   ├── IngredientString.ts
    │   │   │   ├── InventoryEntry.ts
    │   │   │   ├── InventoryLocation.ts
    │   │   │   ├── InventoryLog.ts
    │   │   │   ├── InviteLink.ts
    │   │   │   ├── Keyword.ts
    │   │   │   ├── KeywordLabel.ts
    │   │   │   ├── Localization.ts
    │   │   │   ├── MealPlan.ts
    │   │   │   ├── MealType.ts
    │   │   │   ├── MethodEnum.ts
    │   │   │   ├── NutritionInformation.ts
    │   │   │   ├── OpenDataCategory.ts
    │   │   │   ├── OpenDataConversion.ts
    │   │   │   ├── OpenDataFood.ts
    │   │   │   ├── OpenDataFoodProperty.ts
    │   │   │   ├── OpenDataProperty.ts
    │   │   │   ├── OpenDataStore.ts
    │   │   │   ├── OpenDataStoreCategory.ts
    │   │   │   ├── OpenDataUnit.ts
    │   │   │   ├── OpenDataUnitTypeEnum.ts
    │   │   │   ├── OpenDataVersion.ts
    │   │   │   ├── PaginatedAiLogList.ts
    │   │   │   ├── PaginatedAiProviderList.ts
    │   │   │   ├── PaginatedAutomationList.ts
    │   │   │   ├── PaginatedBookmarkletImportListList.ts
    │   │   │   ├── PaginatedConnectorConfigConfigList.ts
    │   │   │   ├── PaginatedConnectorConfigList.ts
    │   │   │   ├── PaginatedCookLogList.ts
    │   │   │   ├── PaginatedCustomFilterList.ts
    │   │   │   ├── PaginatedEnterpriseSocialEmbedList.ts
    │   │   │   ├── PaginatedEnterpriseSocialRecipeSearchList.ts
    │   │   │   ├── PaginatedEnterpriseSpaceList.ts
    │   │   │   ├── PaginatedExportLogList.ts
    │   │   │   ├── PaginatedFoodList.ts
    │   │   │   ├── PaginatedGenericModelList.ts
    │   │   │   ├── PaginatedGenericModelReferenceList.ts
    │   │   │   ├── PaginatedHouseholdList.ts
    │   │   │   ├── PaginatedImportLogList.ts
    │   │   │   ├── PaginatedIngredientList.ts
    │   │   │   ├── PaginatedInventoryEntryList.ts
    │   │   │   ├── PaginatedInventoryLocationList.ts
    │   │   │   ├── PaginatedInventoryLogList.ts
    │   │   │   ├── PaginatedInviteLinkList.ts
    │   │   │   ├── PaginatedKeywordList.ts
    │   │   │   ├── PaginatedMealPlanList.ts
    │   │   │   ├── PaginatedMealTypeList.ts
    │   │   │   ├── PaginatedOpenDataCategoryList.ts
    │   │   │   ├── PaginatedOpenDataConversionList.ts
    │   │   │   ├── PaginatedOpenDataFoodList.ts
    │   │   │   ├── PaginatedOpenDataPropertyList.ts
    │   │   │   ├── PaginatedOpenDataStoreList.ts
    │   │   │   ├── PaginatedOpenDataUnitList.ts
    │   │   │   ├── PaginatedOpenDataVersionList.ts
    │   │   │   ├── PaginatedPropertyList.ts
    │   │   │   ├── PaginatedPropertyTypeList.ts
    │   │   │   ├── PaginatedRecipeBookEntryList.ts
    │   │   │   ├── PaginatedRecipeBookList.ts
    │   │   │   ├── PaginatedRecipeImportList.ts
    │   │   │   ├── PaginatedRecipeOverviewList.ts
    │   │   │   ├── PaginatedRecipeSimpleList.ts
    │   │   │   ├── PaginatedShoppingListEntryList.ts
    │   │   │   ├── PaginatedShoppingListList.ts
    │   │   │   ├── PaginatedShoppingListRecipeList.ts
    │   │   │   ├── PaginatedSpaceList.ts
    │   │   │   ├── PaginatedStepList.ts
    │   │   │   ├── PaginatedStorageEntryList.ts
    │   │   │   ├── PaginatedStorageList.ts
    │   │   │   ├── PaginatedStorageLocationList.ts
    │   │   │   ├── PaginatedSupermarketCategoryList.ts
    │   │   │   ├── PaginatedSupermarketCategoryRelationList.ts
    │   │   │   ├── PaginatedSupermarketList.ts
    │   │   │   ├── PaginatedSyncList.ts
    │   │   │   ├── PaginatedSyncLogList.ts
    │   │   │   ├── PaginatedUnitConversionList.ts
    │   │   │   ├── PaginatedUnitList.ts
    │   │   │   ├── PaginatedUserFileList.ts
    │   │   │   ├── PaginatedUserSpaceList.ts
    │   │   │   ├── PaginatedViewLogList.ts
    │   │   │   ├── ParsedIngredient.ts
    │   │   │   ├── PatchedAccessToken.ts
    │   │   │   ├── PatchedAiProvider.ts
    │   │   │   ├── PatchedAutomation.ts
    │   │   │   ├── PatchedBookmarkletImport.ts
    │   │   │   ├── PatchedConnectorConfig.ts
    │   │   │   ├── PatchedConnectorConfigConfig.ts
    │   │   │   ├── PatchedCookLog.ts
    │   │   │   ├── PatchedCustomFilter.ts
    │   │   │   ├── PatchedEnterpriseSocialEmbed.ts
    │   │   │   ├── PatchedEnterpriseSpace.ts
    │   │   │   ├── PatchedExportLog.ts
    │   │   │   ├── PatchedFood.ts
    │   │   │   ├── PatchedHousehold.ts
    │   │   │   ├── PatchedImportLog.ts
    │   │   │   ├── PatchedIngredient.ts
    │   │   │   ├── PatchedInventoryEntry.ts
    │   │   │   ├── PatchedInventoryLocation.ts
    │   │   │   ├── PatchedInviteLink.ts
    │   │   │   ├── PatchedKeyword.ts
    │   │   │   ├── PatchedMealPlan.ts
    │   │   │   ├── PatchedMealType.ts
    │   │   │   ├── PatchedOpenDataCategory.ts
    │   │   │   ├── PatchedOpenDataConversion.ts
    │   │   │   ├── PatchedOpenDataFood.ts
    │   │   │   ├── PatchedOpenDataProperty.ts
    │   │   │   ├── PatchedOpenDataStore.ts
    │   │   │   ├── PatchedOpenDataUnit.ts
    │   │   │   ├── PatchedOpenDataVersion.ts
    │   │   │   ├── PatchedProperty.ts
    │   │   │   ├── PatchedPropertyType.ts
    │   │   │   ├── PatchedRecipe.ts
    │   │   │   ├── PatchedRecipeBook.ts
    │   │   │   ├── PatchedRecipeBookEntry.ts
    │   │   │   ├── PatchedRecipeImport.ts
    │   │   │   ├── PatchedSearchPreference.ts
    │   │   │   ├── PatchedShoppingList.ts
    │   │   │   ├── PatchedShoppingListEntry.ts
    │   │   │   ├── PatchedShoppingListRecipe.ts
    │   │   │   ├── PatchedSpace.ts
    │   │   │   ├── PatchedStep.ts
    │   │   │   ├── PatchedStorage.ts
    │   │   │   ├── PatchedStorageEntry.ts
    │   │   │   ├── PatchedStorageLocation.ts
    │   │   │   ├── PatchedSupermarket.ts
    │   │   │   ├── PatchedSupermarketCategory.ts
    │   │   │   ├── PatchedSupermarketCategoryRelation.ts
    │   │   │   ├── PatchedSync.ts
    │   │   │   ├── PatchedUnit.ts
    │   │   │   ├── PatchedUnitConversion.ts
    │   │   │   ├── PatchedUser.ts
    │   │   │   ├── PatchedUserPreference.ts
    │   │   │   ├── PatchedUserSpace.ts
    │   │   │   ├── PatchedViewLog.ts
    │   │   │   ├── Property.ts
    │   │   │   ├── PropertyType.ts
    │   │   │   ├── Recipe.ts
    │   │   │   ├── RecipeBatchUpdate.ts
    │   │   │   ├── RecipeBook.ts
    │   │   │   ├── RecipeBookEntry.ts
    │   │   │   ├── RecipeFlat.ts
    │   │   │   ├── RecipeFromSource.ts
    │   │   │   ├── RecipeFromSourceResponse.ts
    │   │   │   ├── RecipeImage.ts
    │   │   │   ├── RecipeImport.ts
    │   │   │   ├── RecipeOverview.ts
    │   │   │   ├── RecipeShoppingUpdate.ts
    │   │   │   ├── RecipeSimple.ts
    │   │   │   ├── ScalingEnum.ts
    │   │   │   ├── SearchEnum.ts
    │   │   │   ├── SearchFields.ts
    │   │   │   ├── SearchPreference.ts
    │   │   │   ├── ServerSettings.ts
    │   │   │   ├── ShareLink.ts
    │   │   │   ├── ShoppingList.ts
    │   │   │   ├── ShoppingListEntry.ts
    │   │   │   ├── ShoppingListEntryBulk.ts
    │   │   │   ├── ShoppingListEntryBulkCreate.ts
    │   │   │   ├── ShoppingListEntrySimpleCreate.ts
    │   │   │   ├── ShoppingListRecipe.ts
    │   │   │   ├── SourceImportDuplicate.ts
    │   │   │   ├── SourceImportFood.ts
    │   │   │   ├── SourceImportIngredient.ts
    │   │   │   ├── SourceImportKeyword.ts
    │   │   │   ├── SourceImportProperty.ts
    │   │   │   ├── SourceImportPropertyType.ts
    │   │   │   ├── SourceImportRecipe.ts
    │   │   │   ├── SourceImportStep.ts
    │   │   │   ├── SourceImportUnit.ts
    │   │   │   ├── Space.ts
    │   │   │   ├── SpaceNavTextColorEnum.ts
    │   │   │   ├── SpaceThemeEnum.ts
    │   │   │   ├── Step.ts
    │   │   │   ├── Storage.ts
    │   │   │   ├── Supermarket.ts
    │   │   │   ├── SupermarketCategory.ts
    │   │   │   ├── SupermarketCategoryRelation.ts
    │   │   │   ├── Sync.ts
    │   │   │   ├── SyncLog.ts
    │   │   │   ├── ThemeEnum.ts
    │   │   │   ├── TypeEnum.ts
    │   │   │   ├── Unit.ts
    │   │   │   ├── UnitConversion.ts
    │   │   │   ├── User.ts
    │   │   │   ├── UserFile.ts
    │   │   │   ├── UserFileView.ts
    │   │   │   ├── UserPreference.ts
    │   │   │   ├── UserPreferenceNavTextColorEnum.ts
    │   │   │   ├── UserSpace.ts
    │   │   │   ├── ViewLog.ts
    │   │   │   └── index.ts
    │   │   ├── openapi.json
    │   │   ├── openapitools.json
    │   │   ├── runtime.ts
    │   │   └── templates/
    │   │       ├── apis.mustache
    │   │       └── modelGeneric.mustache
    │   ├── pages/
    │   │   ├── 404Page.vue
    │   │   ├── BookViewPage.vue
    │   │   ├── BooksPage.vue
    │   │   ├── DatabasePage.vue
    │   │   ├── HelpPage.vue
    │   │   ├── IngredientEditorPage.vue
    │   │   ├── InventoryBookingPage.vue
    │   │   ├── MealPlanPage.vue
    │   │   ├── ModelDeletePage.vue
    │   │   ├── ModelEditPage.vue
    │   │   ├── ModelListPage.vue
    │   │   ├── PantryPage.vue
    │   │   ├── PropertyEditorPage.vue
    │   │   ├── RecipeImportPage.vue
    │   │   ├── RecipeViewPage.vue
    │   │   ├── SearchPage.vue
    │   │   ├── SettingsPage.vue
    │   │   ├── ShoppingListPage.vue
    │   │   ├── SpaceSetupPage.vue
    │   │   ├── StartPage.vue
    │   │   ├── TestPage.vue
    │   │   └── WelcomePage.vue
    │   ├── service-worker.ts
    │   ├── stores/
    │   │   ├── MealPlanStore.ts
    │   │   ├── MessageStore.ts
    │   │   ├── ShoppingStore.ts
    │   │   └── UserPreferenceStore.ts
    │   ├── types/
    │   │   ├── FoodFilters.ts
    │   │   ├── MealPlan.ts
    │   │   ├── Models.ts
    │   │   ├── Plugins.ts
    │   │   ├── SearchTypes.ts
    │   │   ├── Shopping.ts
    │   │   └── settings.ts
    │   ├── utils/
    │   │   ├── breakpoint_utils.ts
    │   │   ├── cookie.ts
    │   │   ├── date_utils.ts
    │   │   ├── fdc.ts
    │   │   ├── integration_utils.ts
    │   │   ├── logic_utils.ts
    │   │   ├── model_utils.ts
    │   │   ├── number_utils.ts
    │   │   ├── step_utils.ts
    │   │   └── utils.ts
    │   └── vuetify.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    ├── tsconfig.vitest.json
    └── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
omit =
  */apps.py,
  */migrations/*,
  */settings*,
  */test*,
  */tests/*,
  *urls.py,
  */wsgi*,
  manage.py,
  *__init__*


================================================
FILE: .devcontainer/Dockerfile
================================================
FROM python:3.13-alpine3.22

#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn libgcc libstdc++ nginx tini envsubst nodejs npm

#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1

#This port will be used by gunicorn.
EXPOSE 8000

#This port will be used by vue
EXPOSE 8080

#Install all python dependencies to the image
COPY requirements.txt /tmp/pip-tmp/

RUN \
    if [ `apk --print-arch` = "armv7" ]; then \
    printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
    fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl rust && \
    python -m pip install --upgrade pip && \
    pip debug -v && \
    pip install wheel==0.45.1 && \
    pip install setuptools_rust==1.10.2 && \
    pip install -r /tmp/pip-tmp/requirements.txt --no-cache-dir &&\
    apk --purge del .build-deps

================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json.
{
	"name": "Tandoor Dev Container",
	"build": { "context": "..", "dockerfile": "Dockerfile" },

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	"forwardPorts": [8000, 8080],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "pip3 install --user -r requirements.txt"

	// Configure tool-specific properties.
	"customizations": {
		"vscode": {
			"extensions": [
				"ms-python.debugpy",
				"ms-python.python"
			]
		}
	},

	"containerEnv": {
		"CSRF_TRUSTED_ORIGINS": "http://localhost:8000,http://localhost:8080"
	}

	// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
	// "remoteUser": "root"
}


================================================
FILE: .dockerignore
================================================
**/node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.gitignore
README.md
LICENSE
.vscode
.env
.env.template
.github
.idea
.prettierignore
LICENSE.md
docs
update.sh
.pytest_cache
cookbook/tests
mediafiles
staticfiles
db.sqlite3
pytest.ini
vue/**/*.vue
vue/**/*.ts
**/.openapi-generator
mkdocs.yml
vue/babel.config*
vue/package.json
vue/tsconfig.json
vue/src/utils/openapi
venv

================================================
FILE: .flake8
================================================
[flake8]
extend-ignore =
    # Whitespace before ':' - Required for black compatibility
    E203,
    # Line break occurred before a binary operator - Required for black compatibility
    W503,
    # Comparison to False should be 'if cond is False:' or 'if not cond:'
    E712
exclude =
    .git,
    **/__pycache__,
        **/.git,
        **/.svn,
        **/.hg,
        **/CVS,
        **/.DS_Store,
        .vscode,
        **/*.pyc
per-file-ignores=
    cookbook/apps.py:F401
max-line-length = 179



================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: [vabene1111]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md.bak
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

## Version
<!-- Please provide your current version (can be found on the system page since v0.8.4). -->
**Tandoor-Version:** 

## Setup configuration
<!--Please tick all boxes which apply to your configuration. Feel free to provide additional information below.  
To tick boxes here, simply put an X inside the brackets below -->

### Setup
- [ ] Docker / Docker-Compose
- [ ] Unraid
- [ ] Synology
- [ ] Kubernetes
- [ ] Manual setup
- [ ] Others (please state below)

### Reverse Proxy
- [ ] No reverse proxy
- [ ] jwilder's nginx proxy
- [ ] Nginx proxy manager (NPM)
- [ ] SWAG
- [ ] Caddy
- [ ] Traefik
- [ ] Others (please state below)

<!-- Please provide additional information if possible -->
**Additional information:** 

## Bug description
A clear and concise description of what the bug is.



## Logs
<!-- *(Remove this section entirely if no logs are available or necessary for your issue)*  
To get the most information about your issue, set DEBUG=1 (e.g. in your `.env` file if using docker-compose) and try to reproduce the issue afterwards.

Please put your logs into the expandable section below and use code quotation for all logs! Usage: Put three backticks in front and after the log, like this:  
` ``` <Many lines of log messages ``` ` 

Feel free to remove parts if you don't fill them out.
-->

<details>
  <summary>Web-Container-Logs</summary>
    
  <!-- *Put your logs inside here (leave the code quotations in takt):* -->
  
  ```
  Replace me with logs
  ```
</details>

<details>
  <summary>DB-Container-Logs</summary>
    
  <!-- *Put your logs inside here (leave the code quotations in takt):* -->
  
  ```
  Replace me with logs
  ```
</details>

<details>
  <summary>Nginx-Container-Logs <!-- if you use one --></summary>
    
  <!-- *Put your logs inside here (leave the code quotations in takt):* -->
  
  ```
  Replace me with logs
  ```
</details>


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: "Create a report to help us improve"
#title: ""
#labels: ["Bug"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!
  - type: input
    id: version
    attributes:
      label: Tandoor Version
      description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
    validations:
      required: true
  - type: dropdown
    id: setup
    attributes:
      label: Setup
      description: "How is your Tandoor instance set up?"
      options:
        - Docker / Docker-Compose
        - Unraid
        - Synology
        - Kubernetes
        - Manual Setup
        - Others (please state below)
    validations:
      required: true
  - type: dropdown
    id: reverse-proxy
    attributes:
      label: "Reverse Proxy"
      description: "What reverse proxy do you use with Tandoor?"
      options:
        - No reverse proxy
        - jwilder's nginx proxy
        - Nginx Proxy Manager (NPM)
        - SWAG
        - Caddy
        - Traefik
        - Apache2
        - Others (please state below)
    validations:
      required: true
  - type: input
    id: other
    attributes:
      label: Other
      description: "In case you chose 'Others' above, please provide more info here."
  - type: textarea
    id: bug-descr
    attributes:
      label: Bug description
      description: "Please accurately describe the bug you encountered."
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Relevant logs
      description: Please copy and paste any relevant logs. This will be automatically formatted into code, so no need for backticks.
      render: shell


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
  - name: FAQs
    url: https://docs.tandoor.dev/faq/
    about: Please take a look at the FAQs before creating a bug ticket.


================================================
FILE: .github/ISSUE_TEMPLATE/doc_issue.yml
================================================
name: Documentation Issue
description: "Create a report to help us improve"
#title: ""
labels: ["documentation"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this documentation issue report!
  - type: input
    id: docs-link
    attributes:
      label: Documentation link
      description: "Please provide a link to the corresponding documentation site on docs.tandoor.dev"
  - type: dropdown
    id: section
    attributes:
      label: Affected section
      description: "What part of the documentation is the issue about?"
      options:
        - Installation
        - Features
        - System
        - FAQ
        - Does not exist yet
        - Other (please state below)
    validations:
      required: true
  - type: input
    id: other
    attributes:
      label: Other
      description: "In case you chose 'Other' above, please provide more info here."
  - type: textarea
    id: descr
    attributes:
      label: Issue description
      description: "Please accurately describe the documentation issue you are seeing."
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md.bak
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: "Suggest an idea for this project"
#title: ""
labels: ["enhancement"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this feature request!
  - type: textarea
    id: problem
    attributes:
      label: "Is your feature request related to a problem? Please describe."
      description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when..."
  - type: textarea
    id: solution
    attributes:
      label: "Describe the solution you'd like"
      description: "A clear and concise description of what you want to happen."
    validations:
      required: true
  - type: textarea
    id: alternatives
    attributes:
      label: "Describe alternatives you've considered"
      description: "A clear and concise description of any alternative solutions or features you've considered."
  - type: textarea
    id: additional
    attributes:
      label: "Additional context"
      description: "Add any other context or screenshots about the feature request here."


================================================
FILE: .github/ISSUE_TEMPLATE/help-request.md.bak
================================================
---
name: Help request
about: If there is anything wrong with your setup
title: ''
labels: setup issue
assignees: ''

---
## Issue
Please describe your problem here


## Setup Info
Version: (can be found on the system page since v0.8.4)
OS: e.g. Ubuntu 20.02

Other relevant information regarding your problem (proxies, firewalls, etc.)

### `.env`
Please include your `.env` config file (**make sure to remove/replace all secrets**)
```
env content
```

### `docker-compose.yml`
When running with docker compose please provide your `docker-compose.yml`
```
docker-compose.yml content
```

### Logs
If you feel like there is anything interesting please post the output of `docker-compose logs` at
container startup and when the issue happens.

================================================
FILE: .github/ISSUE_TEMPLATE/help_request.yml
================================================
name: Help request
description: "If there is anything wrong with your setup"
#title: ""
labels: ["setup issue"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this help request!
  - type: textarea
    id: issue
    attributes:
      label: Issue
      description: "Please describe your problem here."
    validations:
      required: true
  - type: input
    id: version
    attributes:
      label: Tandoor Version
      description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
    validations:
      required: true
  - type: input
    id: os
    attributes:
      label: OS Version
      description: "E.g. Ubuntu 20.02"
    validations:
      required: true
  - type: dropdown
    id: setup
    attributes:
      label: Setup
      description: "How is your Tandoor instance set up?"
      options:
        - Docker / Docker-Compose
        - Unraid
        - Synology
        - Kubernetes
        - Manual Setup
        - Others (please state below)
    validations:
      required: true
  - type: dropdown
    id: reverse-proxy
    attributes:
      label: "Reverse Proxy"
      description: "What reverse proxy do you use with Tandoor?"
      options:
        - No reverse proxy
        - jwilder's nginx proxy
        - Nginx Proxy Manager (NPM)
        - SWAG
        - Caddy
        - Traefik
        - Others (please state below)
    validations:
      required: true
  - type: input
    id: other
    attributes:
      label: Other
      description: "In case you chose 'Others' above or have more info, please provide additional details here."
  - type: textarea
    id: env
    attributes:
      label: Environment file
      description: "Please include your `.env` config file (**make sure to remove/replace all secrets**)"
      render: shell
  - type: textarea
    id: docker-compose
    attributes:
      label: Docker-Compose file
      description: "When running with docker compose please provide your `docker-compose.yml`"
      render: shell
  - type: textarea
    id: logs
    attributes:
      label: Relevant logs
      description: "If you feel like there is anything interesting please post the output of `docker-compose logs` at container startup and when the issue happens."
      render: shell


================================================
FILE: .github/ISSUE_TEMPLATE/url_import.md.bak
================================================
---
name: Website Import
about: Anything related to website imports
title: ''
labels: enhancement, url_import
assignees: ''

---

### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version: 

### Information
Exact URL you are trying to import from: 

When did the issue happen: When pressing the search button with the url / when importing after the page has loaded

Response/Message shown 
```
Message
```


================================================
FILE: .github/ISSUE_TEMPLATE/website_import.yml
================================================
name: Website Import
description: "Anything related to website imports"
#title: ""
#labels: ["enhancement"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this website import form!
  - type: input
    id: version
    attributes:
      label: Tandoor Version
      description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
    validations:
      required: true
  - type: input
    id: url
    attributes:
      label: Import URL
      description: "Exact URL you are trying to import from."
    validations:
      required: true
  - type: textarea
    id: bug-descr
    attributes:
      label: "When did the issue happen?"
      description: "When pressing the search button with the url / when importing after the page has loaded / ..."
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Response / message shown
      description: Please copy and paste any relevant logs or responses / messages which are shown in Tandoor. This will be automatically formatted into code, so no need for backticks.
      render: shell


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "devcontainers"
    directory: "/"
    schedule:
      interval: weekly

  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "monthly"

  - package-ecosystem: "npm"
    directory: "/vue3/"
    schedule:
      interval: "monthly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"


================================================
FILE: .github/workflows/build-docker.yml
================================================
name: Build Docker Container

on: push

jobs:
  build-container:
    name: Build ${{ matrix.name }} Container
    runs-on: ubuntu-latest
    if: github.repository_owner == 'TandoorRecipes'
    continue-on-error: ${{ matrix.continue-on-error }}
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        include:
          # Standard build config
          - name: Standard
            dockerfile: Dockerfile
            platforms: linux/amd64,linux/arm64
            suffix: ""
            continue-on-error: false
    steps:
      - uses: actions/checkout@v6

      - name: Get version number
        id: get_version
        run: |
          if [[ "$GITHUB_REF" = refs/tags/* ]]; then
            echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
          elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
            echo VERSION=beta >> $GITHUB_OUTPUT
          else
            echo VERSION=develop >> $GITHUB_OUTPUT
          fi

      # Build Vue 3 frontend
      - uses: actions/setup-node@v6
        with:
          node-version: '22'
          cache: yarn
          cache-dependency-path: vue3/yarn.lock
      - name: Install dependencies
        working-directory: ./vue3
        run: yarn install --frozen-lockfile
      - name: Build dependencies
        working-directory: ./vue3
        run: yarn build

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        if: github.secret_source == 'Actions'
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        if: github.secret_source == 'Actions'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ github.token }}
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            vabene1111/recipes
            ghcr.io/TandoorRecipes/recipes
          flavor: |
            latest=false
            suffix=${{ matrix.suffix }}
          tags: |
            type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=ref,event=branch
      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ matrix.dockerfile }}
          pull: true
          push: ${{ github.secret_source == 'Actions' }}
          platforms: ${{ matrix.platforms }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  notify-stable:
    name: Notify Stable
    runs-on: ubuntu-latest
    needs: build-container
    if: startsWith(github.ref, 'refs/tags/')
    steps:
      - name: Set tag name
        run: |
          # Strip "refs/tags/" prefix
          echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
      # Send stable discord notification
      - name: Discord notification
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
        uses: Ilshidur/action-discord@0.4.0
        with:
          args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'

  notify-beta:
    name: Notify Beta
    runs-on: ubuntu-latest
    needs: build-container
    if: github.ref == 'refs/heads/beta'
    steps:
      # Send beta discord notification
      - name: Discord notification
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
        uses: Ilshidur/action-discord@0.4.0
        with:
          args: '🚀 The Tandoor 2 Image has been updated! 🥳'


================================================
FILE: .github/workflows/ci.yml
================================================
name: Continuous Integration

on: [push, pull_request]

jobs:
    build:
        if: github.repository_owner == 'TandoorRecipes'
        runs-on: ubuntu-latest
        strategy:
            max-parallel: 4
            matrix:
                python-version: ["3.12"]
                node-version: ["22"]
        steps:
            - uses: actions/checkout@v6
            - uses: awalsh128/cache-apt-pkgs-action@v1.6.0
              with:
                  packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
                  version: 1.0

            # Setup python & dependencies
            - name: Set up Python ${{ matrix.python-version }}
              uses: actions/setup-python@v6
              with:
                  python-version: ${{ matrix.python-version }}
                  cache: "pip"

            - name: Install Python Dependencies
              run: |
                  python -m pip install --upgrade pip
                  pip install -r requirements.txt

            - name: Cache StaticFiles
              uses: actions/cache@v5
              id: django_cache
              with:
                  path: |
                      ./cookbook/static
                      ./staticfiles
                  key: |
                      ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.node-version }}-collectstatic-${{ hashFiles('**/*.css', '**/*.js', 'vue3/src/*') }}

            # Build Vue frontend & Dependencies
            - name: Set up Node ${{ matrix.node-version }}
              if: steps.django_cache.outputs.cache-hit != 'true'
              uses: actions/setup-node@v6
              with:
                  node-version: ${{ matrix.node-version }}
                  cache: "yarn"
                  cache-dependency-path: ./vue3/yarn.lock

            - name: Install Vue dependencies
              if: steps.django_cache.outputs.cache-hit != 'true'
              working-directory: ./vue3
              run: yarn install

            - name: Build Vue dependencies
              if: steps.django_cache.outputs.cache-hit != 'true'
              working-directory: ./vue3
              run: yarn build

            - name: Compile Django StaticFiles
              if: steps.django_cache.outputs.cache-hit != 'true'
              run: |
                  python3 manage.py collectstatic --noinput

            - uses: actions/cache/save@v5
              if: steps.django_cache.outputs.cache-hit != 'true'
              with:
                  path: |
                      ./cookbook/static
                      ./staticfiles
                  key: |
                      ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.node-version }}-collectstatic-${{ hashFiles('**/*.css', '**/*.js', 'vue/src/*') }}

            - name: Django Testing project
              run: pytest --junitxml=junit/test-results-${{ matrix.python-version }}.xml


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: "Code scanning - action"

on:
  push:
  pull_request:
  schedule:
    - cron: '0 13 * * 2'

jobs:
  CodeQL-Build:
    if: github.repository_owner == 'TandoorRecipes'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          # We must fetch at least the immediate parents so that if this is
          # a pull request then we can checkout the head.
          fetch-depth: 2

      # If this run was triggered by a pull request event, then checkout
      # the head of the pull request instead of the merge commit.
      - run: git checkout HEAD^2
        if: ${{ github.event_name == 'pull_request' }}

      # Initializes the CodeQL tools for scanning.
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v4
        # Override language selection by uncommenting this and choosing your languages
        with:
          languages: python, javascript

      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
      # If this step fails, then you should remove it and run the build manually (see below)
      # - name: Autobuild
      #  uses: github/codeql-action/autobuild@v1

      # ℹ️ Command-line programs to run using the OS shell.
      # 📚 https://git.io/JvXDl

      # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
      #    and modify them (or add more) to build your code if your project
      #    uses a compiled language

      #- run: |
      #   make bootstrap
      #   make release

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v4
        with:
          languages: javascript, python


================================================
FILE: .github/workflows/docs.yml
================================================
name: Make Docs
on:
  # the 1st condition
  workflow_run:
    workflows: ["Continuous Integration"]
    branches: [master]
    types:
      - completed

jobs:
  deploy:
    if: github.repository_owner == 'TandoorRecipes' && ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: 3.x
      - run: pip install mkdocs-material mkdocs-include-markdown-plugin
      - run: mkdocs gh-deploy --force


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
docs/reports/**

# Django stuff:
*.log
mediafiles/
*.sqlite3*
staticfiles/
postgresql/
data/

# Sphinx documentation
docs/_build/

# PyBuilder
target/

\.idea/dataSources/
\.idea/dataSources\.xml

\.idea/dataSources\.local\.xml

\.idea/workspace\.xml

\.idea/misc\.xml

# Deployment
\.env

cookbook/static/vue
vue/webpack-stats.json
/docker-compose.override.yml
vue/node_modules
/recipes/plugins
vetur.config.js
cookbook/static/vue
vue/webpack-stats.json
cookbook/templates/sw.js
vue/.yarn
vue3/.vite

# Configs
vetur.config.js
venv/
.idea/easy-i18n.xml
cookbook/static/vue3
vue3/node_modules
cookbook/tests/other/docs/reports/tests/tests.html
cookbook/tests/other/docs/reports/tests/pytest.xml
vue3/src/plugins
docs/reference/
.idea/vcs.xml
.idea/db-forest-config.xml


================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
  </state>
</component>

================================================
FILE: .idea/dictionaries/vaben.xml
================================================
<component name="ProjectDictionaryState">
  <dictionary name="vaben">
    <words>
      <w>mealplan</w>
      <w>pinia</w>
      <w>selfhosted</w>
      <w>unapplied</w>
    </words>
  </dictionary>
</component>

================================================
FILE: .idea/dictionaries/vabene1111_PC.xml
================================================
<component name="ProjectDictionaryState">
  <dictionary name="vabene1111-PC">
    <words>
      <w>autosync</w>
      <w>chowdown</w>
      <w>csrftoken</w>
      <w>gunicorn</w>
      <w>ical</w>
      <w>invitelink</w>
      <w>mealie</w>
      <w>pepperplate</w>
      <w>safron</w>
      <w>traefik</w>
    </words>
  </dictionary>
</component>

================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
<component name="InspectionProjectProfileManager">
  <settings>
    <option name="PROJECT_PROFILE" value="Default" />
    <option name="USE_PROJECT_PROFILE" value="false" />
    <version value="1.0" />
  </settings>
</component>

================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/recipes.iml" filepath="$PROJECT_DIR$/.idea/recipes.iml" />
    </modules>
  </component>
</project>

================================================
FILE: .idea/prettier.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="PrettierConfiguration">
    <option name="myConfigurationMode" value="DISABLED" />
  </component>
</project>

================================================
FILE: .idea/recipes.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
  <component name="FacetManager">
    <facet type="django" name="Django">
      <configuration>
        <option name="rootFolder" value="$MODULE_DIR$" />
        <option name="settingsModule" value="recipes/settings.py" />
        <option name="manageScript" value="$MODULE_DIR$/manage.py" />
        <option name="environment" value="&lt;map/&gt;" />
        <option name="doNotUseTestRunner" value="false" />
        <option name="trackFilePattern" value="migrations" />
      </configuration>
    </facet>
  </component>
  <component name="NewModuleRootManager">
    <content url="file://$MODULE_DIR$">
      <excludeFolder url="file://$MODULE_DIR$/cookbook/tests/resources" />
      <excludeFolder url="file://$MODULE_DIR$/staticfiles" />
      <excludeFolder url="file://$MODULE_DIR$/venv" />
    </content>
    <orderEntry type="jdk" jdkName="Python 3.12 (recipes)" jdkType="Python SDK" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
  <component name="TemplatesService">
    <option name="TEMPLATE_CONFIGURATION" value="Django" />
    <option name="TEMPLATE_FOLDERS">
      <list>
        <option value="$MODULE_DIR$/cookbook/templates" />
      </list>
    </option>
  </component>
  <component name="TestRunnerService">
    <option name="PROJECT_TEST_RUNNER" value="pytest" />
  </component>
</module>

================================================
FILE: .idea/watcherTasks.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectTasksOptions">
    <TaskOptions isEnabled="false">
      <option name="arguments" value="-m flake8 $FilePath$ --config $ContentRoot$\.flake8" />
      <option name="checkSyntaxErrors" value="true" />
      <option name="description" />
      <option name="exitCodeBehavior" value="NEVER" />
      <option name="fileExtension" value="py" />
      <option name="immediateSync" value="false" />
      <option name="name" value="Flake8 Watcher" />
      <option name="output" value="$FilePath$" />
      <option name="outputFilters">
        <array>
          <FilterInfo>
            <option name="description" value="" />
            <option name="name" value="" />
            <option name="regExp" value="$FILE_PATH$:$LINE$:$COLUMN$: $MESSAGE$" />
          </FilterInfo>
        </array>
      </option>
      <option name="outputFromStdout" value="false" />
      <option name="program" value="$PyInterpreterDirectory$/python" />
      <option name="runOnExternalChanges" value="false" />
      <option name="scopeName" value="Current File" />
      <option name="trackOnlyRoot" value="false" />
      <option name="workingDir" value="" />
      <envs />
    </TaskOptions>
    <TaskOptions isEnabled="false">
      <option name="arguments" value="-m isort $FilePath$" />
      <option name="checkSyntaxErrors" value="true" />
      <option name="description" />
      <option name="exitCodeBehavior" value="ERROR" />
      <option name="fileExtension" value="py" />
      <option name="immediateSync" value="false" />
      <option name="name" value="isort Watcher" />
      <option name="output" value="$FilePath$" />
      <option name="outputFilters">
        <array />
      </option>
      <option name="outputFromStdout" value="false" />
      <option name="program" value="$PyInterpreterDirectory$/python" />
      <option name="runOnExternalChanges" value="false" />
      <option name="scopeName" value="Project Files" />
      <option name="trackOnlyRoot" value="false" />
      <option name="workingDir" value="" />
      <envs />
    </TaskOptions>
    <TaskOptions isEnabled="false">
      <option name="arguments" value="-m yapf -i $FilePath$" />
      <option name="checkSyntaxErrors" value="true" />
      <option name="description" />
      <option name="exitCodeBehavior" value="NEVER" />
      <option name="fileExtension" value="py" />
      <option name="immediateSync" value="false" />
      <option name="name" value="YAPF" />
      <option name="output" value="$FilePath$" />
      <option name="outputFilters">
        <array />
      </option>
      <option name="outputFromStdout" value="false" />
      <option name="program" value="$PyInterpreterDirectory$/python" />
      <option name="runOnExternalChanges" value="false" />
      <option name="scopeName" value="Project Files" />
      <option name="trackOnlyRoot" value="false" />
      <option name="workingDir" value="" />
      <envs />
    </TaskOptions>
    <TaskOptions isEnabled="false">
      <option name="arguments" value="--cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$" />
      <option name="checkSyntaxErrors" value="true" />
      <option name="description" />
      <option name="exitCodeBehavior" value="ERROR" />
      <option name="fileExtension" value="*" />
      <option name="immediateSync" value="true" />
      <option name="name" value="Prettier" />
      <option name="output" value="" />
      <option name="outputFilters">
        <array />
      </option>
      <option name="outputFromStdout" value="false" />
      <option name="program" value="yarn" />
      <option name="runOnExternalChanges" value="true" />
      <option name="scopeName" value="Prettier" />
      <option name="trackOnlyRoot" value="false" />
      <option name="workingDir" value="" />
      <envs />
    </TaskOptions>
  </component>
</project>

================================================
FILE: .prettierignore
================================================
# generated files
api.ts
vue/src/apps/*.js
vue/node_modules
staticfiles/
docs/reports/
/vue3/src/openapi/

# ignored files - prettier interferes with django templates and github actions
*.html
*.yml
*.yaml



================================================
FILE: .prettierrc
================================================
{
    "printWidth": 179,
    "trailingComma": "es5",
    "tabWidth": 2,
    "semi": false,
    "experimentalTernaries": true
}


================================================
FILE: .vscode/launch.json
================================================
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Python Debugger: Django",
        "type": "debugpy",
        "request": "launch",
        "program": "${workspaceFolder}/manage.py",
        "args": ["runserver"],
        "django": true,
        "justMyCode": true
      },
    {
      "name": "Python: Debug Tests",
      "type": "debugpy",
      "request": "launch",
      "program": "${file}",
      "purpose": [
        "debug-test"
      ],
      "console": "integratedTerminal",
      "env": {
        // coverage and pytest can't both be running at the same time
        "PYTEST_ADDOPTS": "--no-cov -n 0"
      },
      "django": true,
      "justMyCode": true
    },
    ]
  }


================================================
FILE: .vscode/settings.json
================================================
{
    "python.testing.pytestArgs": [
        "cookbook/tests"
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.pytestEnabled": true,
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "[python]": {
        "editor.defaultFormatter": "eeyore.yapf",
    },
    "yapf.args": [],
    "isort.args": []
}


================================================
FILE: .vscode/tasks.json
================================================
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Run Migrations",
      "type": "shell",
      "command": "python3 manage.py migrate"
    },
    {
      "label": "Collect Static Files",
      "type": "shell",
      "command": "python3 manage.py collectstatic",
      "dependsOn": ["Yarn Build"]
    },
    {
      "label": "Setup Dev Server",
      "dependsOn": ["Run Migrations"]
    },
    {
      "label": "Run Dev Server",
      "type": "shell", 
      "dependsOn": ["Setup Dev Server"],
      "command": "DEBUG=1 python3 manage.py runserver"
    },
    {
      "label": "Yarn Install",
      "type": "shell",
      "command": "yarn install --force",
      "options": {
        "cwd": "${workspaceFolder}/vue3"
      }
    },
    {
      "label": "Generate API",
      "type": "shell",
      "command": "openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/",
      "options": {
        "cwd": "${workspaceFolder}/vue3/src/openapi"
      }
    },
    {
      "label": "Yarn Dev",
      "type": "shell",
      "command": "yarn dev",
      "dependsOn": ["Yarn Install"],
      "options": {
        "cwd": "${workspaceFolder}/vue3"
      }
    },
    {
      "label": "Yarn Build",
      "type": "shell",
      "command": "yarn build",
      "dependsOn": ["Yarn Install"],
      "options": {
        "cwd": "${workspaceFolder}/vue3"
      },
      "group": "build"
    },
    {
      "label": "Setup Tests",
      "dependsOn": ["Run Migrations", "Collect Static Files"]
    },
    {
      "label": "Run all pytests",
      "type": "shell",
      "command": "python3 -m pytest cookbook/tests",
      "dependsOn": ["Setup Tests"],
      "group": "test"
    },
    {
      "label": "Setup Documentation Dependencies",
      "type": "shell",
      "command": "pip install mkdocs-material mkdocs-include-markdown-plugin"
    },
    {
      "label": "Serve Documentation",
      "type": "shell",
      "command": "mkdocs serve",
      "dependsOn": ["Setup Documentation Dependencies"]
    }
  ]
}


================================================
FILE: CONTRIBUTERS.md
================================================
# Contributers

Many thanks to everyone who contributed to this project! If you add something or help out feel free to add yourself
to this list.

## Code/Features

Please have a look at the [list of pull requests](https://github.com/vabene1111/recipes/pulls) for 
a complete list of contributions.
Below are some of the larger contributions made yet.

- [vabene1111]
- [Kaibu]
- [smilerz]
- [MaxJa4] Docker builds and other improvements
- [tourn] provided the serving feature and **several** other improvements!
- [l0c4lh057] provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277)
- [sebimarkgraf] added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199)
- [cazier] added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88)
- [murphy83] added support for IPv6 #1490
- [TheHaf] added custom serving size component #1411
- [lostlont] added LDAP support #960
- [c0mputerguru] added devcontainers for ease of development

## Translations

### Catalan

[Rubenix](https://www.transifex.com/user/profile/rubenix/)

### Dutch

[D0T1X](https://www.transifex.com/user/profile/D0T1X/)
[ikbenfrank](https://www.transifex.com/user/profile/ikbenfrank/)
[kampsj](https://www.transifex.com/user/profile/kampsj/)

### French

[jt117](https://www.transifex.com/user/profile/jt117/)
[nerdinator](https://www.transifex.com/user/profile/nerdinator/)
[agaume](https://www.transifex.com/user/profile/agaume/)

### German

[eTaurus](https://www.transifex.com/user/profile/eTaurus/)
[l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/)
[hyperbit00](https://github.com/hyperbit00)

### Hungarian

[igazka](https://www.transifex.com/user/profile/igazka/)

### Italian

[SK3LA](https://www.transifex.com/user/profile/SK3LA/)
[auanasgheps](https://www.transifex.com/user/profile/auanasgheps/)

### Latvian

[melkypie](https://github.com/melkypie)

### Portuguese

[hds](https://www.transifex.com/user/profile/hds/)
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
[stormsz](https://www.transifex.com/user/profile/stormsz/)

### Russian

[amillerr](https://github.com/amillerr)

### Spanish

[albertocp](https://www.transifex.com/user/profile/albertocp/)
[alfa5](https://www.transifex.com/user/profile/alfa5/)
[mlopezifu](https://www.transifex.com/user/profile/mlopezifu/)
[sergio.laya](https://www.transifex.com/user/profile/sergio.laya/)

### Swedish

[makanz](https://github.com/makanz)

### Turkish

[batmanisnaked](https://www.transifex.com/user/profile/batmanisnaked/)

### Vietnamese

[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/)


================================================
FILE: Dockerfile
================================================
FROM python:3.13-alpine3.23

#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git libgcc libstdc++ nginx tini envsubst nodejs npm

#Print all logs without buffering it.
ENV PYTHONUNBUFFERED=1 \
    DOCKER=true

#This port will be used by gunicorn.
EXPOSE 80 8080

#Create app dir and install requirements.
RUN mkdir /opt/recipes
WORKDIR /opt/recipes

COPY requirements.txt ./

# remove Development dependencies from requirements.txt
RUN sed -i '/# Development/,$d' requirements.txt
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl rust && \
    python -m venv venv && \
    /opt/recipes/venv/bin/python -m pip install --upgrade pip && \
    venv/bin/pip debug -v && \
    venv/bin/pip install wheel==0.45.1 && \
    venv/bin/pip install setuptools_rust==1.10.2 && \
    venv/bin/pip install -r requirements.txt --no-cache-dir &&\
    apk --purge del .build-deps

#Copy project and execute it.
COPY . ./

RUN <<EOF
    # delete default nginx config and link it to tandoors config
    rm -rf /etc/nginx/http.d
    ln -s /opt/recipes/http.d /etc/nginx/http.d
    # allow all write/read but set sticky bit to restrict non-root to only create files (conf templating in boot.sh)
    chmod 1777 /opt/recipes/http.d/
    # create symlinks to access and error log to show them on stdout
    ln -sf /dev/stdout /var/log/nginx/access.log
    ln -sf /dev/stderr /var/log/nginx/error.log
EOF

# commented for now https://github.com/TandoorRecipes/recipes/issues/3478
#HEALTHCHECK --interval=30s \
#            --timeout=5s \
#            --start-period=10s \
#            --retries=3 \
#            CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/openapi" ]

# collect information from git repositories
RUN /opt/recipes/venv/bin/python version.py
# delete git repositories to reduce image size
RUN find . -type d -name ".git" | xargs rm -rf

RUN chmod +x boot.sh
ENTRYPOINT ["/sbin/tini", "--", "/opt/recipes/boot.sh"]


================================================
FILE: LICENSE.md
================================================
“Commons Clause” License Condition v1.0

The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.

Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.

For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.

Software: https://github.com/vabene1111/recipes

License:  GNU AFFERO GENERAL PUBLIC LICENSE v3

Licensor: https://github.com/vabene1111
               
               
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

---
### Prior 0.10.0
> All versions released before version 0.10. or 29.06.2020 were licensed under the following license

The MIT License (MIT)

Copyright (c) 2018

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================
FILE: README.md
================================================
<h1 align="center">
  <br>
  <a href="https://tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
  <br>
  Tandoor Recipes
  <br>
</h1>

<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>

<p align="center">
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/e/demo-auto-login/" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>

<p align="center">
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>

![Preview](docs/preview.png)

## 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](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- 🏷️ 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](https://schema.org/Recipe)
- ➗ 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](https://docs.tandoor.dev/features/import_export/)
- ➕ 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](https://docs.tandoor.dev/).

## ❤️ 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](https://github.com/sponsors/vabene1111)
- **Patron** You can sponsor contributors of this project on Patron: [vabene111](https://www.patreon.com/cw/vabene1111)
- **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](https://hetzner.cloud/?ref=ISdlrLmr9kGj) 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](https://app.tandoor.dev) where all profits support us and the development of tandoor (currently only available in germany).

## Contributing
Contributions are welcome but please read [this](https://docs.tandoor.dev/contribute/guidelines/) **BEFORE** contributing anything!

## Your Feedback

Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)

## Get in touch

<table>
  <tr>
    <td><a href="https://community.tandoor.dev">Community</a></td>
    <td>Get support, share best practices, discuss feature ideas, and meet other Tandoor users.</td>
  </tr>

  <tr>
    <td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
    <td>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</td>
  </tr>
</table>

## License

Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a
[common clause](https://commonsclause.com/) selling exception. See [LICENSE.md](https://github.com/vabene1111/recipes/blob/develop/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](https://github.com/vabene1111/recipes/issues/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](http://www.gnu.org/philosophy/selling-exceptions.en.html) and the
common clause license is very permissive (see the [FAQ](https://commonsclause.com/)).


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

Since this software is still considered beta/WIP support is always only given for the latest version. There are no backports of security or any other fixes.

## Reporting a Vulnerability

Please use GitHub Security Advisories to report any kind of security vulnerabilities.


================================================
FILE: boot.sh
================================================
#!/bin/sh
source venv/bin/activate

# these are envsubst in the nginx config, make sure they default to something sensible when unset
export TANDOOR_PORT="${TANDOOR_PORT:-80}"
export MEDIA_ROOT=${MEDIA_ROOT:-/opt/recipes/mediafiles};
export STATIC_ROOT=${STATIC_ROOT:-/opt/recipes/staticfiles};

GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
GUNICORN_THREADS="${GUNICORN_THREADS:-2}"
GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"

PLUGINS_BUILD="${PLUGINS_BUILD:-0}"

display_warning() {
    echo "[WARNING]"
    echo -e "$1"
}

# prepare nginx config
envsubst '$MEDIA_ROOT $STATIC_ROOT $TANDOOR_PORT' < /opt/recipes/http.d/Recipes.conf.template > /opt/recipes/http.d/Recipes.conf

# start nginx early to display error pages with writable location as non-root
echo "Starting nginx"
nginx -g 'pid /tmp/nginx.pid;'

echo "Checking configuration..."

# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file

if [ -f "${SECRET_KEY_FILE}" ]; then
    export SECRET_KEY=$(cat "$SECRET_KEY_FILE")
fi

if [ -z "${SECRET_KEY}" ]; then
    display_warning "The environment variable 'SECRET_KEY' (or 'SECRET_KEY_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
fi

if [ -f "${AUTH_LDAP_BIND_PASSWORD_FILE}" ]; then
    export AUTH_LDAP_BIND_PASSWORD=$(cat "$AUTH_LDAP_BIND_PASSWORD_FILE")
fi

if [ -f "${EMAIL_HOST_PASSWORD_FILE}" ]; then
    export EMAIL_HOST_PASSWORD=$(cat "$EMAIL_HOST_PASSWORD_FILE")
fi

if [ -f "${SOCIALACCOUNT_PROVIDERS_FILE}" ]; then
    export SOCIALACCOUNT_PROVIDERS=$(cat "$SOCIALACCOUNT_PROVIDERS_FILE")
fi

if [ -f "${S3_SECRET_ACCESS_KEY_FILE}" ]; then
    export S3_SECRET_ACCESS_KEY=$(cat "$S3_SECRET_ACCESS_KEY_FILE")
fi

echo "Waiting for database to be ready..."

attempt=0
max_attempts=20

if [ "${DB_ENGINE}" == 'django.db.backends.postgresql' ] || [ "${DATABASE_URL}" == 'postgres'* ]; then

  # POSTGRES_PASSWORD (or a valid file at POSTGRES_PASSWORD_FILE) must be set in .env file

  if [ -f "${POSTGRES_PASSWORD_FILE}" ]; then
    export POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE")
  fi

  if [ -z "${POSTGRES_PASSWORD}" ]; then
      display_warning "The environment variable 'POSTGRES_PASSWORD' (or 'POSTGRES_PASSWORD_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
  fi

  while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} --user=${POSTGRES_USER} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
      sleep 5
  done
fi

if [ $attempt -gt $max_attempts ]; then
    echo -e "\nDatabase not reachable. Maximum attempts exceeded."
    echo "Please check logs above - misconfiguration is very likely."
    echo "Make sure the DB container is up and POSTGRES_HOST is set properly."
    echo "Shutting down container."
    exit 1 # exit with error to make the container stop
fi

echo "Database is ready"

echo "Migrating database"

python manage.py migrate

if [ "${PLUGINS_BUILD}" -eq 1 ]; then
    echo "Running yarn build at startup because PLUGINS_BUILD is enabled"
    python plugin.py
fi

echo "Collecting static files, this may take a while..."

python manage.py collectstatic --noinput --clear

echo "Done"

chmod -R 755 ${MEDIA_ROOT:-/opt/recipes/mediafiles}

ipv6_disable=$(cat /sys/module/ipv6/parameters/disable)

echo "Starting gunicorn"
# --umask parameter isn't respected when gunicorn is running in foreground, needed for when users change to non root users
# use /tmp as directory since that is writable as a non-root user
# https://github.com/benoitc/gunicorn/issues/2245
umask 0 && exec gunicorn --bind unix:/tmp/tandoor.sock --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout ${GUNICORN_TIMEOUT:-30} --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi


================================================
FILE: cookbook/__init__.py
================================================


================================================
FILE: cookbook/admin.py
================================================
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import SearchVector
from django.utils import translation
from django_scopes import scopes_disabled
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from allauth.account.decorators import secure_admin_login

from cookbook.managers import DICTIONARY

from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, ImportLog, Ingredient, InviteLink,
                     Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
                     Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
                     ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
                     Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
                     TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
                     ViewLog, ConnectorConfig, AiProvider, AiLog)

admin.site.login = secure_admin_login(admin.site.login)


class CustomUserAdmin(UserAdmin):
    def has_add_permission(self, request, obj=None):
        return False


admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

admin.site.unregister(Group)


@admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset):
    for space in queryset:
        space.safe_delete()


class SpaceAdmin(admin.ModelAdmin):
    list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
    search_fields = ('name', 'created_by__username')
    autocomplete_fields = ('created_by',)
    filter_horizontal = ('food_inherit',)
    list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
    date_hierarchy = 'created_at'
    actions = [delete_space_action]


admin.site.register(Space, SpaceAdmin)


class UserSpaceAdmin(admin.ModelAdmin):
    list_display = ('user', 'space',)
    search_fields = ('user__username', 'space__name',)
    filter_horizontal = ('groups',)
    autocomplete_fields = ('user', 'space',)


admin.site.register(UserSpace, UserSpaceAdmin)


class UserPreferenceAdmin(admin.ModelAdmin):
    list_display = ('name', 'theme', 'default_page')
    search_fields = ('user__username',)
    list_filter = ('theme', 'default_page',)
    date_hierarchy = 'created_at'
    filter_horizontal = ('plan_share', 'shopping_share',)

    @staticmethod
    def name(obj):
        return obj.user.get_user_display_name()


admin.site.register(UserPreference, UserPreferenceAdmin)


class SearchPreferenceAdmin(admin.ModelAdmin):
    list_display = ('name', 'search', 'trigram_threshold',)
    search_fields = ('user__username',)
    list_filter = ('search',)

    @staticmethod
    def name(obj):
        return obj.user.get_user_display_name()


admin.site.register(SearchPreference, SearchPreferenceAdmin)


class AiProviderAdmin(admin.ModelAdmin):
    list_display = ('name', 'space', 'model_name',)
    search_fields = ('name', 'space', 'model_name',)


admin.site.register(AiProvider, AiProviderAdmin)


class AiLogAdmin(admin.ModelAdmin):
    list_display = ('ai_provider', 'function', 'credit_cost', 'created_by', 'created_at',)

admin.site.register(AiLog, AiLogAdmin)


class StorageAdmin(admin.ModelAdmin):
    list_display = ('name', 'method')
    search_fields = ('name',)


admin.site.register(Storage, StorageAdmin)


class ConnectorConfigAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'type', 'enabled', 'url')
    search_fields = ('name', 'url')


admin.site.register(ConnectorConfig, ConnectorConfigAdmin)


class CustomFilterAdmin(admin.ModelAdmin):
    list_display = ('id', 'type', 'name')


admin.site.register(CustomFilter, CustomFilterAdmin)


class SyncAdmin(admin.ModelAdmin):
    list_display = ('storage', 'path', 'active', 'last_checked')
    search_fields = ('storage__name', 'path')


admin.site.register(Sync, SyncAdmin)


class SupermarketCategoryInline(admin.TabularInline):
    model = SupermarketCategoryRelation


class SupermarketAdmin(admin.ModelAdmin):
    list_display = ('name', 'space',)
    inlines = (SupermarketCategoryInline,)


class SupermarketCategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'space',)


admin.site.register(Supermarket, SupermarketAdmin)
admin.site.register(SupermarketCategory, SupermarketCategoryAdmin)


class SyncLogAdmin(admin.ModelAdmin):
    list_display = ('sync', 'status', 'msg', 'created_at')


admin.site.register(SyncLog, SyncLogAdmin)


@admin.action(description='Temporarily ENABLE sorting on Foods and Keywords.')
def enable_tree_sorting(modeladmin, request, queryset):
    Food.node_order_by = ['name']
    Keyword.node_order_by = ['name']
    with scopes_disabled():
        Food.fix_tree(fix_paths=True)
        Keyword.fix_tree(fix_paths=True)


@admin.action(description='Temporarily DISABLE sorting on Foods and Keywords.')
def disable_tree_sorting(modeladmin, request, queryset):
    Food.node_order_by = []
    Keyword.node_order_by = []


@admin.action(description='Fix problems and sort tree by name')
def sort_tree(modeladmin, request, queryset):
    orginal_value = modeladmin.model.node_order_by[:]
    modeladmin.model.node_order_by = ['name']
    with scopes_disabled():
        modeladmin.model.fix_tree(fix_paths=True)
    modeladmin.model.node_order_by = orginal_value


class KeywordAdmin(TreeAdmin):
    form = movenodeform_factory(Keyword)
    ordering = ('space', 'path',)
    search_fields = ('name',)
    actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]


admin.site.register(Keyword, KeywordAdmin)


@admin.action(description='Delete Steps not part of a Recipe.')
def delete_unattached_steps(modeladmin, request, queryset):
    with scopes_disabled():
        Step.objects.filter(recipe=None).delete()


class StepAdmin(admin.ModelAdmin):
    list_display = ('recipe_and_name', 'order', 'space')
    ordering = ('recipe__name', 'name', 'space',)
    search_fields = ('name', 'recipe__name')
    actions = [delete_unattached_steps]

    @staticmethod
    @admin.display(description="Name")
    def recipe_and_name(obj):
        if not obj.recipe_set.exists():
            return "Orphaned Step" + ('' if not obj.name else f': {obj.name}')
        return f"{obj.recipe_set.first().name}: {obj.name}" if obj.name else obj.recipe_set.first().name


admin.site.register(Step, StepAdmin)


@admin.action(description='Rebuild index for selected recipes')
def rebuild_index(modeladmin, request, queryset):
    language = DICTIONARY.get(translation.get_language(), 'simple')
    with scopes_disabled():
        Recipe.objects.all().update(
            name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
            desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)
        )
        Step.objects.all().update(search_vector=SearchVector('instruction__unaccent', weight='B', config=language))


class RecipeAdmin(admin.ModelAdmin):
    list_display = ('name', 'internal', 'created_by', 'storage', 'space')
    search_fields = ('name', 'created_by__username')
    ordering = ('name', 'created_by__username',)
    list_filter = ('internal',)
    date_hierarchy = 'created_at'

    @staticmethod
    def created_by(obj):
        return obj.created_by.get_user_display_name()

    if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
        actions = [rebuild_index]


admin.site.register(Recipe, RecipeAdmin)


class UnitAdmin(admin.ModelAdmin):
    list_display = ('name', 'space')
    ordering = ('name', 'space',)
    search_fields = ('name',)


admin.site.register(Unit, UnitAdmin)


# admin.site.register(FoodInheritField)


class FoodAdmin(TreeAdmin):
    form = movenodeform_factory(Keyword)
    ordering = ('space', 'path',)
    search_fields = ('name',)
    actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]


admin.site.register(Food, FoodAdmin)


class UnitConversionAdmin(admin.ModelAdmin):
    list_display = ('base_amount', 'base_unit', 'food', 'converted_amount', 'converted_unit')
    search_fields = ('food__name', 'unit__name')


admin.site.register(UnitConversion, UnitConversionAdmin)


@admin.action(description='Delete Ingredients not part of a Recipe.')
def delete_unattached_ingredients(modeladmin, request, queryset):
    with scopes_disabled():
        Ingredient.objects.filter(step__recipe=None).delete()


class IngredientAdmin(admin.ModelAdmin):
    list_display = ('recipe_name', 'amount', 'unit', 'food', 'space')
    search_fields = ('food__name', 'unit__name', 'step__recipe__name')
    actions = [delete_unattached_ingredients]

    @staticmethod
    @admin.display(description="Recipe")
    def recipe_name(obj):
        recipes = obj.step_set.first().recipe_set.all() if obj.step_set.exists() else None
        return recipes.first().name if recipes else 'Orphaned Ingredient'


admin.site.register(Ingredient, IngredientAdmin)


class CommentAdmin(admin.ModelAdmin):
    list_display = ('recipe', 'name', 'created_at')
    search_fields = ('text', 'created_by__username')
    date_hierarchy = 'created_at'

    @staticmethod
    def name(obj):
        return obj.created_by.get_user_display_name()


admin.site.register(Comment, CommentAdmin)


class RecipeImportAdmin(admin.ModelAdmin):
    list_display = ('name', 'storage', 'file_path')


admin.site.register(RecipeImport, RecipeImportAdmin)


class RecipeBookAdmin(admin.ModelAdmin):
    list_display = ('name', 'user_name', 'space')
    search_fields = ('name', 'created_by__username')

    @staticmethod
    def user_name(obj):
        return obj.created_by.get_user_display_name()


admin.site.register(RecipeBook, RecipeBookAdmin)


class RecipeBookEntryAdmin(admin.ModelAdmin):
    list_display = ('book', 'recipe')


admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)


class MealPlanAdmin(admin.ModelAdmin):
    list_display = ('user', 'recipe', 'meal_type', 'from_date', 'to_date')

    @staticmethod
    def user(obj):
        return obj.created_by.get_user_display_name()


admin.site.register(MealPlan, MealPlanAdmin)


class MealTypeAdmin(admin.ModelAdmin):
    list_display = ('name', 'space', 'created_by', 'order')
    search_fields = ('name', 'space', 'created_by__username')


admin.site.register(MealType, MealTypeAdmin)


class ViewLogAdmin(admin.ModelAdmin):
    list_display = ('recipe', 'created_by', 'created_at')


admin.site.register(ViewLog, ViewLogAdmin)


class InviteLinkAdmin(admin.ModelAdmin):
    list_display = (
        'group', 'valid_until', 'space',
        'created_by', 'created_at', 'used_by'
    )


admin.site.register(InviteLink, InviteLinkAdmin)


class CookLogAdmin(admin.ModelAdmin):
    list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
    search_fields = ('recipe__name', 'space__name',)


admin.site.register(CookLog, CookLogAdmin)


class ShoppingListRecipeAdmin(admin.ModelAdmin):
    list_display = ('id', 'recipe', 'servings')


admin.site.register(ShoppingListRecipe, ShoppingListRecipeAdmin)


class ShoppingListEntryAdmin(admin.ModelAdmin):
    list_display = ('id', 'food', 'unit', 'list_recipe', 'created_by', 'created_at', 'checked')


admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)


class ShareLinkAdmin(admin.ModelAdmin):
    list_display = ('recipe', 'created_by', 'uuid', 'created_at',)


admin.site.register(ShareLink, ShareLinkAdmin)


@admin.action(description='Delete all properties with type')
def delete_properties_with_type(modeladmin, request, queryset):
    for pt in queryset:
        Property.objects.filter(property_type=pt).delete()


class PropertyTypeAdmin(admin.ModelAdmin):
    search_fields = ('name',)

    list_display = ('id', 'space', 'name', 'fdc_id')
    actions = [delete_properties_with_type]


admin.site.register(PropertyType, PropertyTypeAdmin)


class PropertyAdmin(admin.ModelAdmin):
    list_display = ('property_amount', 'property_type')


admin.site.register(Property, PropertyAdmin)


class NutritionInformationAdmin(admin.ModelAdmin):
    list_display = ('id',)


admin.site.register(NutritionInformation, NutritionInformationAdmin)


class ImportLogAdmin(admin.ModelAdmin):
    list_display = ('id', 'type', 'running', 'created_by', 'created_at',)


admin.site.register(ImportLog, ImportLogAdmin)


class TelegramBotAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'created_by',)


admin.site.register(TelegramBot, TelegramBotAdmin)


class BookmarkletImportAdmin(admin.ModelAdmin):
    list_display = ('id', 'url', 'created_by', 'created_at',)


admin.site.register(BookmarkletImport, BookmarkletImportAdmin)


class UserFileAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'file_size_kb', 'created_at',)


admin.site.register(UserFile, UserFileAdmin)


================================================
FILE: cookbook/apps.py
================================================
import traceback

from django.apps import AppConfig
from django.conf import settings
from django.db import OperationalError, ProgrammingError
from django.db.models.signals import post_save, post_delete
from django_scopes import scopes_disabled

from recipes.settings import DEBUG


class CookbookConfig(AppConfig):
    name = 'cookbook'

    def ready(self):
        import cookbook.signals  # noqa

        if not settings.DISABLE_EXTERNAL_CONNECTORS:
            from cookbook.connectors.connector_manager import ConnectorManager  # Needs to be here to prevent loading race condition of oauth2 modules in models.py
            handler = ConnectorManager()
            post_save.connect(handler, dispatch_uid="post_save-connector_manager")
            post_delete.connect(handler, dispatch_uid="post_delete-connector_manager")

        # if not settings.DISABLE_TREE_FIX_STARTUP:
        #     # when starting up run fix_tree to:
        #     #   a) make sure that nodes are sorted when switching between sort modes
        #     #   b) fix problems, if any, with tree consistency
        #     with scopes_disabled():
        #         try:
        #             from cookbook.models import Food, Keyword
        #             Keyword.fix_tree(fix_paths=True)
        #             Food.fix_tree(fix_paths=True)
        #         except OperationalError:
        #             if DEBUG:
        #                 traceback.print_exc()
        #             pass  # if model does not exist there is no need to fix it
        #         except ProgrammingError:
        #             if DEBUG:
        #                 traceback.print_exc()
        #             pass  # if migration has not been run database cannot be fixed yet
        #         except Exception:
        #             if DEBUG:
        #                 traceback.print_exc()
        #             pass  # dont break startup just because fix could not run, need to investigate cases when this happens

================================================
FILE: cookbook/connectors/__init__.py
================================================


================================================
FILE: cookbook/connectors/connector.py
================================================
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional

from cookbook.models import ShoppingListEntry, User, ConnectorConfig


@dataclass
class UserDTO:
    username: str
    first_name: Optional[str]

    @staticmethod
    def create_from_user(instance: User) -> 'UserDTO':
        return UserDTO(
            username=instance.username,
            first_name=instance.first_name if instance.first_name else None
        )


@dataclass
class ShoppingListEntryDTO:
    food_name: str
    amount: Optional[float]
    base_unit: Optional[str]
    unit_name: Optional[str]
    created_by: UserDTO

    @staticmethod
    def try_create_from_entry(instance: ShoppingListEntry) -> Optional['ShoppingListEntryDTO']:
        if instance.food is None or instance.created_by is None:
            return None

        return ShoppingListEntryDTO(
            food_name=instance.food.name,
            amount=instance.amount if instance.amount else None,
            unit_name=instance.unit.name if instance.unit else None,
            base_unit=instance.unit.base_unit if instance.unit and instance.unit.base_unit else None,
            created_by=UserDTO.create_from_user(instance.created_by),
        )


# A Connector is 'destroyed' & recreated each time 'any' ConnectorConfig in a space changes.
class Connector(ABC):
    @abstractmethod
    def __init__(self, config: ConnectorConfig):
        pass

    @abstractmethod
    async def on_shopping_list_entry_created(self, instance: ShoppingListEntryDTO) -> None:
        pass

    # This method might not trigger on 'direct' entry updates: https://stackoverflow.com/a/35238823
    @abstractmethod
    async def on_shopping_list_entry_updated(self, instance: ShoppingListEntryDTO) -> None:
        pass

    @abstractmethod
    async def on_shopping_list_entry_deleted(self, instance: ShoppingListEntryDTO) -> None:
        pass

    @abstractmethod
    async def close(self) -> None:
        pass


================================================
FILE: cookbook/connectors/connector_manager.py
================================================
import asyncio
import logging
import queue
import threading
from asyncio import Task
from dataclasses import dataclass
from enum import Enum
from logging import Logger
from types import UnionType
from typing import List, Any, Dict, Optional, Type

from django.conf import settings
from django_scopes import scope

from cookbook.connectors.connector import Connector, ShoppingListEntryDTO
from cookbook.connectors.homeassistant import HomeAssistant
from cookbook.models import ShoppingListEntry, Space, ConnectorConfig

REGISTERED_CLASSES: UnionType | Type = ShoppingListEntry


class ActionType(Enum):
    CREATED = 1
    UPDATED = 2
    DELETED = 3


@dataclass
class Work:
    instance: REGISTERED_CLASSES | ConnectorConfig
    actionType: ActionType


class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


# The way ConnectionManager works is as follows:
# 1. On init, it starts a worker & creates a queue for 'Work'
# 2. Then any time its called, it verifies the type of action (create/update/delete) and if the item is of interest, pushes the Work (non-blocking) to the queue.
# 3. The worker consumes said work from the queue.
# 3.1 If the work is of type ConnectorConfig, it flushes its cache of known connectors (per space.id)
# 3.2 If work is of type REGISTERED_CLASSES, it asynchronously fires of all connectors and wait for them to finish (runtime should depend on the 'slowest' connector)
# 4. Work is marked as consumed, and next entry of the queue is consumed.
# Each 'Work' is processed in sequential by the worker, so the throughput is about [workers * the slowest connector]
# The Singleton class is used for ConnectorManager to have a self-reference and so Python does not garbage collect it
class ConnectorManager(metaclass=Singleton):
    _logger: Logger
    _queue: queue.Queue
    _listening_to_classes = REGISTERED_CLASSES | ConnectorConfig

    def __init__(self):
        self._logger = logging.getLogger("recipes.connector")
        self._logger.debug("ConnectorManager initializing")
        self._queue = queue.Queue(maxsize=settings.EXTERNAL_CONNECTORS_QUEUE_SIZE)
        self._worker = threading.Thread(target=self.worker, args=(0, self._queue,), daemon=True)
        self._worker.start()

    # Called by post save & post delete signals
    def __call__(self, instance: Any, **kwargs) -> None:
        action_type: ActionType
        if "created" in kwargs and kwargs["created"]:
            action_type = ActionType.CREATED
        elif "created" in kwargs and not kwargs["created"]:
            action_type = ActionType.UPDATED
        elif "origin" in kwargs:
            action_type = ActionType.DELETED
        else:
            return

        self._add_work(action_type, instance)

    def _add_work(self, action_type: ActionType, *instances: REGISTERED_CLASSES):
        for instance in instances:
            if not isinstance(instance, self._listening_to_classes) or not hasattr(instance, "space"):
                continue
            try:
                _force_load_instance(instance)
                self._queue.put_nowait(Work(instance, action_type))
            except queue.Full:
                self._logger.info(f"queue was full, so skipping {action_type} of type {type(instance)}")

    def stop(self):
        self._queue.put(None)
        self._worker.join(timeout=5)

    @classmethod
    def is_initialized(cls):
        return cls in cls._instances

    @staticmethod
    def add_work(action_type: ActionType, *instances: REGISTERED_CLASSES):
        """
        Manually inject work that failed to come in through the __call__ (aka Django signal)
        Before the work is processed, we check if the connectionManager is initialized, because if it's not, we don't want to accidentally initialize it.
        Be careful calling it, because it might result in a instance being processed twice.
        """
        if not ConnectorManager.is_initialized():
            return
        ConnectorManager()._add_work(action_type, *instances)

    @staticmethod
    def worker(worker_id: int, worker_queue: queue.Queue):
        logger = logging.getLogger("recipes.connector.worker")

        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        logger.info(f"started ConnectionManager worker {worker_id}")

        # When multiple workers are used, please make sure the cache is shared across all threads, otherwise it might lead to un-expected behavior.
        _connectors_cache: Dict[int, List[Connector]] = dict()

        while True:
            try:
                item: Optional[Work] = worker_queue.get()
            except KeyboardInterrupt:
                break

            if item is None:
                break

            logger.debug(f"received {item.instance=} with {item.actionType=}")

            # If a Connector was changed/updated, refresh connector from the database for said space
            refresh_connector_cache = isinstance(item.instance, ConnectorConfig)

            space: Space = item.instance.space
            connectors: Optional[List[Connector]] = _connectors_cache.get(space.id)

            if connectors is None or refresh_connector_cache:
                if connectors is not None:
                    loop.run_until_complete(_close_connectors(connectors))

                with scope(space=space):
                    connectors: List[Connector] = list()
                    for config in space.connectorconfig_set.all():
                        config: ConnectorConfig = config
                        if not config.enabled:
                            continue

                        try:
                            connector: Optional[Connector] = ConnectorManager.get_connected_for_config(config)
                        except BaseException:
                            logger.exception(f"failed to initialize {config.name}")
                            continue

                        if connector is not None:
                            connectors.append(connector)

                    _connectors_cache[space.id] = connectors

            if len(connectors) == 0 or refresh_connector_cache:
                worker_queue.task_done()
                continue

            logger.debug(f"running {len(connectors)} connectors for {item.instance=} with {item.actionType=}")

            loop.run_until_complete(run_connectors(connectors, item.instance, item.actionType))
            worker_queue.task_done()

        logger.info(f"terminating ConnectionManager worker {worker_id}")

        from django.db import connection
        connection.close()

        asyncio.set_event_loop(None)
        loop.close()

    @staticmethod
    def get_connected_for_config(config: ConnectorConfig) -> Optional[Connector]:
        match config.type:
            case ConnectorConfig.HOMEASSISTANT:
                return HomeAssistant(config)
            case _:
                return None


def _force_load_instance(instance: REGISTERED_CLASSES):
    if isinstance(instance, ShoppingListEntry):
        _ = instance.food  # Force load food
        _ = instance.unit  # Force load unit
        _ = instance.created_by  # Force load created_by


async def _close_connectors(connectors: List[Connector]):
    tasks: List[Task] = [asyncio.create_task(connector.close()) for connector in connectors]

    if len(tasks) == 0:
        return

    try:
        await asyncio.gather(*tasks, return_exceptions=False)
    except BaseException:
        logging.exception("received an exception while closing one of the connectors")


async def run_connectors(connectors: List[Connector], instance: REGISTERED_CLASSES, action_type: ActionType):
    tasks: List[Task] = list()

    if isinstance(instance, ShoppingListEntry):
        shopping_list_entry = ShoppingListEntryDTO.try_create_from_entry(instance)
        if shopping_list_entry is None:
            return

        match action_type:
            case ActionType.CREATED:
                for connector in connectors:
                    tasks.append(asyncio.create_task(connector.on_shopping_list_entry_created(shopping_list_entry)))
            case ActionType.UPDATED:
                for connector in connectors:
                    tasks.append(asyncio.create_task(connector.on_shopping_list_entry_updated(shopping_list_entry)))
            case ActionType.DELETED:
                for connector in connectors:
                    tasks.append(asyncio.create_task(connector.on_shopping_list_entry_deleted(shopping_list_entry)))

    if len(tasks) == 0:
        return

    try:
        # Wait for all async tasks to finish, if one fails, the others still continue.
        await asyncio.gather(*tasks, return_exceptions=False)
    except BaseException:
        logging.exception("received an exception from one of the connectors")


================================================
FILE: cookbook/connectors/homeassistant.py
================================================
import logging
from logging import Logger
from typing import Dict, Tuple
from urllib.parse import urljoin

from aiohttp import request, ClientResponseError

from cookbook.connectors.connector import Connector, ShoppingListEntryDTO
from cookbook.models import ConnectorConfig


class HomeAssistant(Connector):
    _config: ConnectorConfig
    _logger: Logger

    def __init__(self, config: ConnectorConfig):
        if not config.token or not config.url or not config.todo_entity:
            raise ValueError("config for HomeAssistantConnector in incomplete")

        self._logger = logging.getLogger(f"recipes.connector.homeassistant.{config.name}")

        if config.url[-1] != "/":
            config.url += "/"
        self._config = config

    async def homeassistant_api_call(self, method: str, path: str, data: Dict) -> str:
        headers = {
            "Authorization": f"Bearer {self._config.token}",
            "Content-Type": "application/json"
        }
        async with request(method, urljoin(self._config.url, path), headers=headers, json=data) as response:
            response.raise_for_status()
            return await response.json()

    async def on_shopping_list_entry_created(self, shopping_list_entry: ShoppingListEntryDTO) -> None:
        if not self._config.on_shopping_list_entry_created_enabled:
            return

        item, description = _format_shopping_list_entry(shopping_list_entry)

        self._logger.debug(f"adding {item=} with {description=} to {self._config.todo_entity}")

        data = {
            "entity_id": self._config.todo_entity,
            "item": item,
        }

        if self._config.supports_description_field:
            data["description"] = description

        try:
            await self.homeassistant_api_call("POST", "services/todo/add_item", data)
        except ClientResponseError as err:
            self._logger.warning(f"received an exception from the api: {err.request_info.url=}, {err.request_info.method=}, {err.status=}, {err.message=}, {type(err)=}")

    async def on_shopping_list_entry_updated(self, shopping_list_entry: ShoppingListEntryDTO) -> None:
        if not self._config.on_shopping_list_entry_updated_enabled:
            return
        pass

    async def on_shopping_list_entry_deleted(self, shopping_list_entry: ShoppingListEntryDTO) -> None:
        if not self._config.on_shopping_list_entry_deleted_enabled:
            return

        item, _ = _format_shopping_list_entry(shopping_list_entry)

        self._logger.debug(f"removing {item=} from {self._config.todo_entity}")

        data = {
            "entity_id": self._config.todo_entity,
            "item": item,
        }

        try:
            await self.homeassistant_api_call("POST", "services/todo/remove_item", data)
        except ClientResponseError as err:
            # This error will always trigger if the item is not present/found
            self._logger.debug(f"received an exception from the api: {err.request_info.url=}, {err.request_info.method=}, {err.status=}, {err.message=}, {type(err)=}")

    async def close(self) -> None:
        pass


def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntryDTO) -> Tuple[str, str]:
    item = shopping_list_entry.food_name
    if shopping_list_entry.amount:
        item += f" ({shopping_list_entry.amount:.2f}".rstrip('0').rstrip('.')
        if shopping_list_entry.base_unit:
            item += f" {shopping_list_entry.base_unit})"
        elif shopping_list_entry.unit_name:
            item += f" {shopping_list_entry.unit_name})"
        else:
            item += ")"

    description = "From TandoorRecipes"
    if shopping_list_entry.created_by.first_name:
        description += f", by {shopping_list_entry.created_by.first_name}"
    else:
        description += f", by {shopping_list_entry.created_by.username}"

    return item, description


================================================
FILE: cookbook/fixtures/german_example_tags.json
================================================
[
  {
    "model": "cookbook.keyword",
    "pk": 1,
    "fields": {
      "name": "Hauptgang",
      "icon": "\ud83c\udf7d",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:07:08.957Z",
      "updated_at": "2019-11-15T14:07:08.957Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 2,
    "fields": {
      "name": "Vorspeise",
      "icon": "\ud83e\udd63",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:07:31.863Z",
      "updated_at": "2019-11-15T14:07:31.863Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 3,
    "fields": {
      "name": "Nachtisch",
      "icon": "\ud83c\udf68",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:07:49.873Z",
      "updated_at": "2019-11-15T14:07:49.873Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 4,
    "fields": {
      "name": "Salat",
      "icon": "\ud83e\udd57",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:07:58.033Z",
      "updated_at": "2019-11-15T14:07:58.034Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 5,
    "fields": {
      "name": "Rind",
      "icon": "\ud83d\udc04",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:08:24.881Z",
      "updated_at": "2019-11-15T14:08:24.881Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 6,
    "fields": {
      "name": "Schwein",
      "icon": "\ud83d\udc16",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:08:35.754Z",
      "updated_at": "2019-11-15T14:08:35.754Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 7,
    "fields": {
      "name": "Huhn",
      "icon": "\ud83d\udc14",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:08:43.360Z",
      "updated_at": "2019-11-15T14:08:43.360Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 8,
    "fields": {
      "name": "Wild",
      "icon": "\ud83e\udd8c",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:08:52.137Z",
      "updated_at": "2019-11-15T14:08:52.137Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 9,
    "fields": {
      "name": "Kuchen",
      "icon": "\ud83c\udf70",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:09:00.433Z",
      "updated_at": "2019-11-15T14:09:00.433Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 10,
    "fields": {
      "name": "Nudeln",
      "icon": "\ud83c\udf5d",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:09:25.839Z",
      "updated_at": "2019-11-15T14:09:25.839Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 11,
    "fields": {
      "name": "Reis",
      "icon": "\ud83c\udf5a",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:09:31.879Z",
      "updated_at": "2019-11-15T14:09:31.879Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 12,
    "fields": {
      "name": "Kartoffeln",
      "icon": "\ud83e\udd54",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:09:39.023Z",
      "updated_at": "2019-11-15T14:09:39.023Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 13,
    "fields": {
      "name": "Suppe",
      "icon": "\ud83c\udf72",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:14:54.922Z",
      "updated_at": "2019-11-15T14:14:54.922Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 14,
    "fields": {
      "name": "Thermomix",
      "icon": "\u2699",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:15:35.806Z",
      "updated_at": "2019-11-15T14:15:35.806Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 15,
    "fields": {
      "name": "S\u00fc\u00df",
      "icon": "\ud83c\udf6f",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:19:03.339Z",
      "updated_at": "2019-11-15T14:19:03.339Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 16,
    "fields": {
      "name": "Auflauf",
      "icon": "\ud83e\udd58",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:19:21.874Z",
      "updated_at": "2019-11-15T14:19:21.874Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 17,
    "fields": {
      "name": "Backen",
      "icon": "\ud83d\udd25",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:21:19.399Z",
      "updated_at": "2019-11-15T14:21:19.399Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 18,
    "fields": {
      "name": "Fisch",
      "icon": "\ud83d\udc1f",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-15T14:23:32.303Z",
      "updated_at": "2019-11-15T14:23:32.303Z"
    }
  },
  {
    "model": "cookbook.keyword",
    "pk": 19,
    "fields": {
      "name": "Dip",
      "icon": "\ud83e\udd6b",
      "description": "",
      "created_by": 0,
      "created_at": "2019-11-16T13:32:42.625Z",
      "updated_at": "2019-11-16T13:43:28.404Z"
    }
  }
]

================================================
FILE: cookbook/forms.py
================================================
from datetime import datetime

from allauth.account.forms import ResetPasswordForm, SignupForm
from allauth.socialaccount.forms import SignupForm as SocialSignupForm
from django import forms
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField
from hcaptcha.fields import hCaptchaField

from .models import InviteLink, Recipe, Space, User, UserPreference, UserSpace


class SelectWidget(widgets.Select):

    class Media:
        js = ('custom/js/form_select.js', )


class MultiSelectWidget(widgets.SelectMultiple):

    class Media:
        js = ('custom/js/form_multiselect.js', )


class ImportExportBase(forms.Form):
    DEFAULT = 'DEFAULT'
    PAPRIKA = 'PAPRIKA'
    NEXTCLOUD = 'NEXTCLOUD'
    MEALIE = 'MEALIE'
    MEALIE1 = 'MEALIE1'
    CHOWDOWN = 'CHOWDOWN'
    SAFFRON = 'SAFFRON'
    CHEFTAP = 'CHEFTAP'
    PEPPERPLATE = 'PEPPERPLATE'
    RECIPEKEEPER = 'RECIPEKEEPER'
    RECETTETEK = 'RECETTETEK'
    RECIPESAGE = 'RECIPESAGE'
    DOMESTICA = 'DOMESTICA'
    MEALMASTER = 'MEALMASTER'
    MELARECIPES = 'MELARECIPES'
    REZKONV = 'REZKONV'
    OPENEATS = 'OPENEATS'
    PLANTOEAT = 'PLANTOEAT'
    COOKBOOKAPP = 'COOKBOOKAPP'
    COOKLANG = 'COOKLANG'
    COPYMETHAT = 'COPYMETHAT'
    COOKMATE = 'COOKMATE'
    REZEPTSUITEDE = 'REZEPTSUITEDE'
    PDF = 'PDF'
    GOURMET = 'GOURMET'

    type = forms.ChoiceField(
        choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (MEALIE1, 'Mealie1'), (CHOWDOWN, 'Chowdown'),
                 (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
                 (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'),
                 (COOKLANG, 'Cooklang Markdown'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'), (COOKMATE, 'Cookmate'),
                 (REZEPTSUITEDE, 'Recipesuite.de'), (GOURMET, 'Gourmet'))
    )



class MultipleFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class MultipleFileField(forms.FileField):

    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

    def clean(self, data, initial=None):
        single_file_clean = super().clean
        if isinstance(data, (list, tuple)):
            result = [single_file_clean(d, initial) for d in data]
        else:
            result = single_file_clean(data, initial)
        return result


class ImportForm(ImportExportBase):
    files = MultipleFileField(required=True)
    duplicates = forms.BooleanField(
        help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False
    )
    meal_plans = forms.BooleanField(required=False)
    shopping_lists = forms.BooleanField(required=False)
    nutrition_per_serving = forms.BooleanField(required=False)  # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose


class ExportForm(ImportExportBase):
    recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
    all = forms.BooleanField(required=False)
    custom_filter = forms.IntegerField(required=False)

    def __init__(self, *args, **kwargs):
        space = kwargs.pop('space')
        super().__init__(*args, **kwargs)
        self.fields['recipes'].queryset = Recipe.objects.filter(space=space).all()


class InviteLinkForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user')
        super().__init__(*args, **kwargs)
        self.fields['space'].queryset = Space.objects.filter(created_by=user).all()

    def clean(self):
        space = self.cleaned_data['space']
        if space.max_users != 0 and (
            UserPreference.objects.filter(space=space).count() + InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=space).count()
        ) >= space.max_users:
            raise ValidationError(_('Maximum number of users for this space reached.'))

    def clean_email(self):
        email = self.cleaned_data['email']
        with scopes_disabled():
            if email != '' and User.objects.filter(email=email).exists():
                raise ValidationError(_('Email address already taken!'))

        return email

    class Meta:
        model = InviteLink
        fields = ('email', 'group', 'valid_until', 'space')
        help_texts = {
            'email': _('An email address is not required but if present the invite link will be sent to the user.'),
        }
        field_classes = {
            'space': SafeModelChoiceField,
        }


class SpaceCreateForm(forms.Form):
    prefix = 'create'
    name = forms.CharField()

    def clean_name(self):
        name = self.cleaned_data['name']
        with scopes_disabled():
            if Space.objects.filter(name=name).exists():
                raise ValidationError(_('Name already taken.'))
        return name


class SpaceJoinForm(forms.Form):
    prefix = 'join'
    token = forms.CharField()


class AllAuthSignupForm(SignupForm):
    captcha = hCaptchaField()
    terms = forms.BooleanField(label=_('Accept Terms and Privacy'))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if settings.PRIVACY_URL == '' and settings.TERMS_URL == '':
            self.fields.pop('terms')
        if settings.HCAPTCHA_SECRET == '':
            self.fields.pop('captcha')

    def signup(self, request, user):
        pass


class AllAuthSocialSignupForm(SocialSignupForm):
    terms = forms.BooleanField(label=_('Accept Terms and Privacy'))

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if settings.PRIVACY_URL == '' and settings.TERMS_URL == '':
            self.fields.pop('terms')

    def signup(self, request, user):
        if settings.SOCIAL_DEFAULT_ACCESS:
            with scopes_disabled():
                space = Space.objects.first()
                if space:
                    user_space = UserSpace.objects.create(
                        space=space, user=user, active=True
                    )
                    user_space.groups.add(
                        Group.objects.filter(name=settings.SOCIAL_DEFAULT_GROUP).get()
                    )


class CustomPasswordResetForm(ResetPasswordForm):
    captcha = hCaptchaField()

    def __init__(self, **kwargs):
        super(CustomPasswordResetForm, self).__init__(**kwargs)
        if settings.HCAPTCHA_SECRET == '':
            self.fields.pop('captcha')


class UserCreateForm(forms.Form):
    name = forms.CharField(label='Username')
    password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))
    password_confirm = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}))


================================================
FILE: cookbook/helper/AllAuthCustomAdapter.py
================================================

import datetime
import logging

from gettext import gettext as _

from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from django.utils import timezone

from cookbook.models import InviteLink

logger = logging.getLogger(__name__)


class AllAuthCustomAdapter(DefaultAccountAdapter):

    def is_open_for_signup(self, request):
        """
        Whether to allow sign-ups.
        """
        signup_token = False
        if 'signup_token' in request.session and InviteLink.objects.filter(
                valid_until__gte=timezone.now().date(), used_by=None, uuid=request.session['signup_token']).exists():
            signup_token = True

        if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP and not signup_token:
            return False
        elif request.resolver_match.view_name == 'socialaccount_signup' and len(settings.SOCIAL_PROVIDERS) < 1:
            return False
        else:
            return super(AllAuthCustomAdapter, self).is_open_for_signup(request)

    # disable password reset for now
    def send_mail(self, template_prefix, email, context):
        if settings.EMAIL_HOST != '':
            default = timezone.now()
            c = caches['default'].get_or_set(email, default, timeout=360)
            if c == default:
                try:
                    super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
                except Exception as e:  # dont fail signup just because confirmation mail could not be send
                    logger.error(f"Failed to send {template_prefix} email to {email}: {type(e).__name__}: {e}")
            else:
                messages.add_message(self.request, messages.ERROR, _('In order to prevent spam, the requested email was not send. Please wait a few minutes and try again.'))
        else:
            logger.debug(f"Email not sent (EMAIL_HOST not configured): {template_prefix} to {email}")


================================================
FILE: cookbook/helper/CustomStorageClass.py
================================================
import hashlib

from django.conf import settings
from django.core.cache import cache
from storages.backends.s3boto3 import S3Boto3Storage


class CachedS3Boto3Storage(S3Boto3Storage):
    def url(self, name, **kwargs):
        key = hashlib.md5(f'recipes_media_urls_{name}'.encode('utf-8')).hexdigest()
        if result := cache.get(key):
            return result

        result = super(CachedS3Boto3Storage, self).url(name, **kwargs)

        timeout = int(settings.AWS_QUERYSTRING_EXPIRE * .95)
        cache.set(key, result, timeout)

        return result


================================================
FILE: cookbook/helper/HelperFunctions.py
================================================
import socket
import requests
import struct
from ipaddress import ip_address
from urllib.parse import urlparse, quote, urlunparse

from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db.models import Func
from thefuzz import fuzz
from thefuzz import process as fuzz_process
from requests_hardened import Config, Manager


class Round(Func):
    function = 'ROUND'
    template = '%(function)s(%(expressions)s, 0)'


def str2bool(v):
    if isinstance(v, bool) or v is None:
        return v
    else:
        return v.lower() in ("yes", "true", "1")


def safe_request(method, url, **kwargs):
    """
    use requests-hardened to make external requests SSRF safe
    """
    http_manager = Manager(
        Config(
            default_timeout=(2, 10),
            never_redirect=False,
            # Enable SSRF IP filter
            ip_filter_enable=True,
            ip_filter_allow_loopback_ips=False,
        )
    )
    return http_manager.send_request(method, url, **kwargs)


def match_or_fuzzymatch(check_string: str, key_dict: dict) -> tuple[str, int]:
    """
    takes a string and sees if it matches exactly any of the Dictionary keys
    or any of the alternative strings listed in the value of each key.
    If there are no matches return the key of the string that returns the best fuzzy match against your check_string.

    :param check_string: A string that you want to attempt to match
    :param key_dict: key: exact terms you are searching for, value:a list of strings that are alternative terms to check.
    :return:
    """
    score = (None, 0)
    for key in key_dict:
        key_dict[key].append(key)
        if check_string.lower() in [match.lower() for match in key_dict[key]]:
            return (key, 100)
    for key in key_dict:
        key_score = fuzz_process.extract(check_string, key_dict[key], limit=1, scorer=fuzz.partial_token_sort_ratio)[0]
        if key_score[1] > score[1]:
            score = (key, key_score[1])
    return score


================================================
FILE: cookbook/helper/__init__.py
================================================
from cookbook.helper.AllAuthCustomAdapter import AllAuthCustomAdapter

__all__ = [
]


================================================
FILE: cookbook/helper/ai_helper.py
================================================
from decimal import Decimal

from django.utils import timezone
from django.db.models import Sum
from litellm import CustomLogger

from cookbook.models import AiLog
from recipes import settings


def get_monthly_token_usage(space):
    """
    returns the number of credits the space has used in the current month
    """
    token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
    if token_usage is None:
        token_usage = 0
    return token_usage


def has_monthly_token(space):
    """
    checks if the monthly credit limit has been exceeded
    """
    return get_monthly_token_usage(space) < space.ai_credits_monthly


def can_perform_ai_request(space):
    return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled


class AiCallbackHandler(CustomLogger):
    space = None
    user = None
    ai_provider = None
    function = None

    def __init__(self, space, user, ai_provider, function):
        super().__init__()
        self.space = space
        self.user = user
        self.ai_provider = ai_provider
        self.function = function

    def log_pre_api_call(self, model, messages, kwargs):
        pass

    def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
        pass

    def log_success_event(self, kwargs, response_obj, start_time, end_time):
        self.create_ai_log(kwargs, response_obj, start_time, end_time)

    def log_failure_event(self, kwargs, response_obj, start_time, end_time):
        self.create_ai_log(kwargs, response_obj, start_time, end_time)

    def create_ai_log(self, kwargs, response_obj, start_time, end_time):
        credit_cost = 0
        credits_from_balance = False
        if self.ai_provider.log_credit_cost:
            credit_cost = kwargs.get("response_cost", 0) * 100

        if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
            remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
            if remaining_balance < 0:
                remaining_balance = 0
                if settings.HOSTED and self.space.ai_credits_monthly == 0:
                    self.space.ai_enabled = False

            self.space.ai_credits_balance = remaining_balance
            credits_from_balance = True
            self.space.save()

        AiLog.objects.create(
            created_by=self.user,
            space=self.space,
            ai_provider=self.ai_provider,
            start_time=start_time,
            end_time=end_time,
            input_tokens=response_obj['usage']['prompt_tokens'],
            output_tokens=response_obj['usage']['completion_tokens'],
            function=self.function,
            credit_cost=credit_cost,
            credits_from_balance=credits_from_balance,
        )


================================================
FILE: cookbook/helper/automation_helper.py
================================================
import re

from django.core.cache import caches
from django.db.models.functions import Lower

from cookbook.models import Automation


class AutomationEngine:
    request = None
    source = None
    use_cache = None
    food_aliases = None
    keyword_aliases = None
    unit_aliases = None
    never_unit = None
    transpose_words = None
    regex_replace = {
        Automation.DESCRIPTION_REPLACE: None,
        Automation.INSTRUCTION_REPLACE: None,
        Automation.FOOD_REPLACE: None,
        Automation.UNIT_REPLACE: None,
        Automation.NAME_REPLACE: None,
    }

    def __init__(self, request, use_cache=True, source=None):
        self.request = request
        self.use_cache = use_cache
        if not source:
            self.source = "default_string_to_avoid_false_regex_match"
        else:
            self.source = source

    def apply_keyword_automation(self, keyword):
        keyword = keyword.strip()
        if self.use_cache and self.keyword_aliases is None:
            self.keyword_aliases = {}
            KEYWORD_CACHE_KEY = f'automation_keyword_alias_{self.request.space.pk}'
            if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
                self.keyword_aliases = c
                caches['default'].touch(KEYWORD_CACHE_KEY, 30)
            else:
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
                    self.keyword_aliases[a.param_1.lower()] = a.param_2
                caches['default'].set(KEYWORD_CACHE_KEY, self.keyword_aliases, 30)
        else:
            self.keyword_aliases = {}
        if self.keyword_aliases:
            try:
                keyword = self.keyword_aliases[keyword.lower()]
            except KeyError:
                pass
        else:
            if automation := Automation.objects.filter(space=self.request.space, type=Automation.KEYWORD_ALIAS, param_1__iexact=keyword, disabled=False).order_by('order').first():
                return automation.param_2
        return keyword

    def apply_unit_automation(self, unit):
        unit = unit.strip()
        if self.use_cache and self.unit_aliases is None:
            self.unit_aliases = {}
            UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
            if c := caches['default'].get(UNIT_CACHE_KEY, None):
                self.unit_aliases = c
                caches['default'].touch(UNIT_CACHE_KEY, 30)
            else:
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
                    self.unit_aliases[a.param_1.lower()] = a.param_2
                caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
        else:
            self.unit_aliases = {}
        if self.unit_aliases:
            try:
                unit = self.unit_aliases[unit.lower()]
            except KeyError:
                pass
        else:
            if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first():
                return automation.param_2
        return self.apply_regex_replace_automation(unit, Automation.UNIT_REPLACE)

    def apply_food_automation(self, food):
        food = food.strip()
        if self.use_cache and self.food_aliases is None:
            self.food_aliases = {}
            FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
            if c := caches['default'].get(FOOD_CACHE_KEY, None):
                self.food_aliases = c
                caches['default'].touch(FOOD_CACHE_KEY, 30)
            else:
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
                    self.food_aliases[a.param_1.lower()] = a.param_2
                caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
        else:
            self.food_aliases = {}

        if self.food_aliases:
            try:
                return self.food_aliases[food.lower()]
            except KeyError:
                return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)
        else:
            if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
                return automation.param_2
        return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)

    def apply_never_unit_automation(self, tokens):
        """
        Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
        e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
        or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
        :param1 tokens: string that should never be considered a unit, will be moved to token[2]
        :param2 (optional) unit as string: will insert unit string into token[1]
        :return: unit as string (possibly changed by automation)
        """

        if self.use_cache and self.never_unit is None:
            self.never_unit = {}
            NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}'
            if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None):
                self.never_unit = c
                caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30)
            else:
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all():
                    self.never_unit[a.param_1.lower()] = a.param_2
                caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30)
        else:
            self.never_unit = {}

        new_unit = None
        alt_unit = self.apply_unit_automation(tokens[1])
        never_unit = False
        if self.never_unit:
            try:
                new_unit = self.never_unit[tokens[1].lower()]
                never_unit = True
            except KeyError:
                return tokens, never_unit
        else:
            if a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
                    tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
                new_unit = a.param_2
                never_unit = True

        if never_unit:
            tokens.insert(1, new_unit)
        return tokens, never_unit

    def apply_transpose_automation(self, string):
        """
        If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string
        :param 1: first word to detect
        :param 2: second word to detect
        return: new ingredient string
        """
        if self.use_cache and self.transpose_words is None:
            self.transpose_words = {}
            TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}'
            if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None):
                self.transpose_words = c
                caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30)
            else:
                i = 0
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only(
                        'param_1', 'param_2').order_by('order').all()[:512]:
                    self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()]
                    i += 1
                caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30)
        else:
            self.transpose_words = {}

        tokens = [x.lower() for x in string.replace(',', ' ').split()]
        if self.transpose_words:
            for key, value in self.transpose_words.items():
                if value[0] in tokens and value[1] in tokens:
                    string = re.sub(rf"\b({value[0]})\W*({value[1]})\b", r"\2 \1", string, flags=re.IGNORECASE)
        else:
            for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \
                    .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \
                    .filter(param_1_lower__in=tokens, param_2_lower__in=tokens).order_by('order')[:512]:
                if rule.param_1 in tokens and rule.param_2 in tokens:
                    string = re.sub(rf"\b({rule.param_1})\W*({rule.param_2})\b", r"\2 \1", string, flags=re.IGNORECASE)
        return string

    def apply_regex_replace_automation(self, string, automation_type):
        # TODO add warning - maybe on SPACE page? when a max of 512 automations of a specific type is exceeded (ALIAS types excluded?)
        """
        Replaces strings in a recipe field that are from a matched source
        field_type are Automation.type that apply regex replacements
        Automation.DESCRIPTION_REPLACE
        Automation.INSTRUCTION_REPLACE
        Automation.FOOD_REPLACE
        Automation.UNIT_REPLACE
        Automation.NAME_REPLACE

        regex replacment utilized the following fields from the Automation model
        :param 1: source that should apply the automation in regex format ('.*' for all)
        :param 2: regex pattern to match ()
        :param 3: replacement string (leave blank to delete)
        return: new string
        """
        if self.use_cache and self.regex_replace[automation_type] is None:
            self.regex_replace[automation_type] = {}
            REGEX_REPLACE_CACHE_KEY = f'automation_regex_replace_{self.request.space.pk}'
            if c := caches['default'].get(REGEX_REPLACE_CACHE_KEY, None):
                self.regex_replace[automation_type] = c[automation_type]
                caches['default'].touch(REGEX_REPLACE_CACHE_KEY, 30)
            else:
                i = 0
                for a in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
                        'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
                    self.regex_replace[automation_type][i] = [a.param_1, a.param_2, a.param_3]
                    i += 1
                caches['default'].set(REGEX_REPLACE_CACHE_KEY, self.regex_replace, 30)
        else:
            self.regex_replace[automation_type] = {}

        if self.regex_replace[automation_type]:
            for rule in self.regex_replace[automation_type].values():
                if re.match(rule[0], (self.source)[:512]):
                    string = re.sub(rule[1], rule[2], string, flags=re.IGNORECASE)
        else:
            for rule in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
                    'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
                if re.match(rule.param_1, (self.source)[:512]):
                    string = re.sub(rule.param_2, rule.param_3, string, flags=re.IGNORECASE)
        return string


================================================
FILE: cookbook/helper/batch_edit_helper.py
================================================
def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
    """
    given a model, the base and related field and the base and related ids, bulk create relation objects
    """
    relation_objects = []
    for b in base_ids:
        for r in related_ids:
            relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r}))
    relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,))


def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
    relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete()


def remove_all_from_relation(relation_model, base_field_name, base_ids):
    relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete()


def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
    remove_all_from_relation(relation_model, base_field_name, base_ids)
    add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids)


================================================
FILE: cookbook/helper/cache_helper.py
================================================
class CacheHelper:
    space = None

    BASE_UNITS_CACHE_KEY = None
    PROPERTY_TYPE_CACHE_KEY = None

    def __init__(self, space):
        self.space = space

        self.BASE_UNITS_CACHE_KEY = f'SPACE_{space.id}_BASE_UNITS'
        self.PROPERTY_TYPE_CACHE_KEY = f'SPACE_{space.id}_PROPERTY_TYPES'


================================================
FILE: cookbook/helper/context_processors.py
================================================
from django.conf import settings


def context_settings(request):
    return {
        'EMAIL_ENABLED': settings.EMAIL_HOST != '',
        'SIGNUP_ENABLED': settings.ENABLE_SIGNUP,
        'CAPTCHA_ENABLED': settings.HCAPTCHA_SITEKEY != '',
        'HOSTED': settings.HOSTED,
        'TERMS_URL': settings.TERMS_URL,
        'PRIVACY_URL': settings.PRIVACY_URL,
        'IMPRINT_URL': settings.IMPRINT_URL,
        'SHOPPING_MIN_AUTOSYNC_INTERVAL': settings.SHOPPING_MIN_AUTOSYNC_INTERVAL,
        'DISABLE_EXTERNAL_CONNECTORS': settings.DISABLE_EXTERNAL_CONNECTORS,
        'HIDE_LOGIN_FORM': settings.HIDE_LOGIN_FORM,
    }


================================================
FILE: cookbook/helper/cooklang_parser.py
================================================
# Cooklang parser forked from on https://github.com/luizribeiro/py-cooklang cooklang.py as of 11/18/25 - MIT License
# Modifications by doylelew

import copy
import re
from dataclasses import dataclass
from typing import Optional, Union


@dataclass
class Quantity:
    amount: Union[str, float]
    unit: Optional[str] = None

    @classmethod
    def parse(cls, raw) -> "Quantity":
        amount = raw
        unit = ""
        if len(quantity_tokens := re.split(r'%', amount)) == 2:
            amount = quantity_tokens[0]
            unit = quantity_tokens[1]
        if amount != "":
            try:
                float(amount)
            except ValueError:
                whole_num = 0
                if len(integer_fraction_tokens := re.split(r' ', amount)) == 2:
                    whole_num = int(integer_fraction_tokens[0])
                    amount = integer_fraction_tokens[1]

                if len(fraction := re.split(r'/', amount)) == 2:
                    amount = int(fraction[0]) / int(fraction[1])
                    amount += whole_num
                else:
                    raise Exception(f"Cooklang:Quantity:parse Syntax Error: measurements must be in float or #/# format, received {amount}")
            amount = round(float(amount), 3)
        return Quantity(amount=amount, unit=unit)

    @classmethod
    def add_optional(cls, a: Optional["Quantity"], b: Optional["Quantity"]) -> Optional["Quantity"]:
        if a and b:
            return a + b
        elif a or b:
            return a or b
        return None

    def __add__(self, other: "Quantity") -> "Quantity":
        if self.unit != other.unit:
            raise ValueError(f"Cannot add unit {self.unit} to {other.unit}")
        new_amount = self.amount + other.amount
        return Quantity(
            amount=new_amount,
            unit=self.unit,
        )


@dataclass
class Timer:
    ingredient_str: str
    quantity: float
    unit: str

    @classmethod
    def parse(cls, raw) -> "Timer":
        if len(timer_tokens := re.split(r'[{%]', raw)) < 3:
            raise Exception(f"Cooklang:Recipe:Timer Syntax Error: timers must be in {{num%unit}} format, received {raw}")
        ingredient = timer_tokens[0]
        quantity = timer_tokens[1]
        unit = timer_tokens[2].replace("}", "")

        try:
            float(quantity)
        except ValueError:
            whole_num = 0
            if len(integer_fraction_tokens := re.split(r' ', quantity)) == 2:
                whole_num = int(integer_fraction_tokens[0])
                quantity = integer_fraction_tokens[1]
            if len(fraction := re.split(r'/', quantity)) == 2:
                quantity = int(fraction[0]) / int(fraction[1])
                quantity += whole_num
            else:
                raise Exception(f"Cooklang:Timer:parse Syntax Error: measurements must be in float or #/# format, received {quantity}")
        quantity = round(float(quantity), 3)
        return Timer(ingredient_str=ingredient, quantity=quantity, unit=unit)


@dataclass
class Ingredient:
    name: str
    quantity: Optional[Quantity] = None

    @classmethod
    def parse(cls, raw: str) -> "Ingredient":
        ingredient = raw
        quantity = ""
        if len(ingredient_tokens := re.split(r'{', raw)) == 2:
            ingredient = ingredient_tokens[0]
            quantity = ingredient_tokens[1].replace('}', '')
        return Ingredient(
            name=ingredient,
            quantity=Quantity.parse(quantity),
        )

    def __add__(self, other: "Ingredient") -> "Ingredient":
        if self.name != other.name:
            raise ValueError(f"Cannot add ingredient {self.name} with {other.name}", )
        return Ingredient(
            name=self.name,
            quantity=Quantity.add_optional(self.quantity, other.quantity),
        )


@dataclass
class Block:
    type: str
    value: Union[str, Ingredient, Timer]

    @classmethod
    def new(cls):
        return Block(type="", value="")


@dataclass
class Step:
    blocks: list[Block]

    @classmethod
    def parse(cls, raw: str) -> "Step":
        # split steps into a stream of tokens broken up by delimiters
        blocks = []
        token_stream = re.split(r'(--|\[-|-\]|[@#}~])', raw)

        # verify terminating delimiter and return token or if no delimiter then return only first word
        def find_arbitrary_termination_token(current_token, next_token, termination_delimiter) -> tuple[str, list[str]]:
            return_tokens = []
            if next_token != termination_delimiter:
                sub_tokens = re.split(r'(?=[\W\s])([^-])', current_token)
                current_token = sub_tokens.pop(0)
                if next_token:
                    return_tokens.insert(0, next_token)
                return_tokens.insert(0, "".join(sub_tokens))
            else:
                current_token += termination_delimiter
            return current_token, return_tokens

        # ensure that the correct terminating delimiter is used
        def find_termination_token(current_token, next_token, termination_delimiter) -> str:
            if next_token != termination_delimiter:
                raise Exception(f"Cooklang:Recipe:parse Syntax Error: expected terminating delimiter {termination_delimiter}, recieved {next_token}")
            else:
                return current_token + termination_delimiter

        # asses tokens in order to identify and denote datatypes into blocks
        while token_stream:
            block = Block.new()
            token = token_stream.pop(0)
            stream_return = []
            match token:
                case "@":
                    block.type = "Ingredient"
                    token_next = token_stream.pop(0)
                    lookahead = None
                    if len(token_stream) > 0:
                        lookahead = token_stream.pop(0)
                    token, stream_return = find_arbitrary_termination_token(token_next, lookahead, '}')
                    block.value = Ingredient.parse(token)
                case "#":
                    block.type = "Cookware"
                    token_next = token_stream.pop(0)
                    lookahead = None
                    if len(token_stream) > 0:
                        lookahead = token_stream.pop(0)
                    token, stream_return = find_arbitrary_termination_token(token_next, lookahead, '}')
                    block.value = token.replace("{}", "")
                case "~":
                    block.type = "Timer"
                    token_next = token_stream.pop(0)
                    lookahead = None
                    if len(token_stream) > 0:
                        lookahead = token_stream.pop(0)
                    token, stream_return = find_arbitrary_termination_token(token_next, lookahead, '}')
                    block.value = Timer.parse(token)
                case "--":
                    block.type = "inline comment"
                    token = token_stream.pop(0)
                    if len(find_new_line := token.split("\n", 1)) == 2:
                        block.value = find_new_line[0]
                        stream_return = ["\n" + find_new_line[1]]
                    elif len(token_stream) == 0:
                        block.value = token
                    else:
                        raise Exception("Cooklang:Step:parse Syntax Error: inline comments must end with a new line")
                case "[-":
                    block.type = "block comment"
                    block.value = find_termination_token(token_stream.pop(0), token_stream.pop(0), '-]').replace("-]", "")
                case "}":
                    raise Exception("Cooklang:Step:parse Syntax Error: stray '}' found with no delimiting '@', '#' or '~'")
                case "-]":
                    raise Exception("Cooklang:Step:parse Syntax Error: stray '-]' found with no opening '[-'")
                case _:
                    block.type = "text"
                    block.value = token
            token_stream = stream_return + token_stream
            if block.value != '':
                blocks.append(block)

        return Step(blocks=blocks)


@dataclass
class Recipe:
    metadata: dict[str, str]
    ingredients: list[Ingredient]
    steps: list[Step]

    @classmethod
    def parse(cls, raw: str) -> "Recipe":
        # Remove white space at the end of the document
        raw = re.sub(r'\s+$', '', raw)

        # Separate the Metadata from the rest of the recipe, "--- metadata ---" format
        raw_metadata = ""
        raw_no_metadata = raw
        if len(raw_meta_split := re.split(r'---([\s\S]*?)---\n', raw)) > 1:
            raw_metadata = raw_meta_split[-2]
            raw_no_metadata = raw_meta_split[-1]

        # Separate the Metadata from the rest of the recipe, ">> metadata" format
        if not raw_metadata:
            for line in raw.lstrip().splitlines():
                if not bool(re.match(r'^>>', line)):
                    break
                raw_no_metadata = raw_no_metadata.replace(line, "")
                raw_metadata = raw_metadata + re.sub(r'^>>', "", line).lstrip() + "\n"
        raw_metadata.rstrip()

        # Parse the metadata
        metadata = {}
        if raw_metadata:
            current_key = None
            for line in raw_metadata.splitlines():
                if len(key_value_pair := re.split(":", line, maxsplit=1)) > 1:
                    current_key, value = key_value_pair
                    metadata[current_key] = value.lstrip()
                elif re.match(r'^\s*-\s', line) and current_key is not None:
                    metadata[current_key] = metadata[current_key] + f"{re.split(r'^\s*-\s', line)[-1]}, "

        # Parse the Steps recursively Step -> Ingredient -> Quantity
        raw_steps = re.split(r'\n\n', raw_no_metadata.lstrip())
        steps = [Step.parse(step.lstrip()) for step in raw_steps]
        for step in steps:
            if not step.blocks:
                steps.remove(step)

        # using the blocks in the steps, calculate global ingredients
        def add_ingredient_to_global(global_dict, ingredient_object) -> dict[str:Ingredient]:
            if ingredient_object.name in global_dict.keys():
                try:
                    global_dict[ingredient_object.name] += ingredient_object
                except ValueError:
                    new_ingredient_object = copy.deepcopy(ingredient_object)
                    new_ingredient_object.name = f"{ingredient_object.name}({ingredient_object.quantity.unit})"
                    global_dict = add_ingredient_to_global(global_dict, new_ingredient_object)

            else:
                global_dict[ingredient_object.name] = copy.deepcopy(ingredient_object)
            return global_dict

        ingredient_dict = {}
        ingredient_blocks = [item for sublist in [[block.value for block in step.blocks if block.type == "Ingredient"] for step in steps] for item in sublist]
        for ingredient in ingredient_blocks:
            ingredient_dict = add_ingredient_to_global(ingredient_dict, ingredient)

        return Recipe(
            metadata=metadata,
            ingredients=[ingredient_dict[key] for key in ingredient_dict.keys()],
            steps=steps,
        )


================================================
FILE: cookbook/helper/drf_spectacular_hooks.py
================================================
# custom processing for schema
# reason: DRF writable nested needs ID's to decide if a nested object should be created or updated
# the API schema/client make ID's read only by default and strips them entirely in request objects (with COMPONENT_SPLIT_REQUEST enabled)
# change the schema to make IDs optional but writable so they are included in the request

def custom_postprocessing_hook(result, generator, request, public):
    for c in result['components']['schemas'].keys():
        # handle schemas used by the client to do requests on the server
        if 'properties' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['properties']:
            # make ID field not read only so it's not stripped from the request on the client
            result['components']['schemas'][c]['properties']['id']['readOnly'] = False
            # make ID field not required
            if 'required' in result['components']['schemas'][c] and 'id' in result['components']['schemas'][c]['required']:
                result['components']['schemas'][c]['required'].remove('id')

    return result


# TODO remove below once legacy API has been fully deprecated
from drf_spectacular.openapi import AutoSchema  # noqa: E402 isort: skip
import functools  # noqa: E402 isort: skip
import re  # noqa: E402 isort: skip


class LegacySchema(AutoSchema):
    operation_id_base = None

    @functools.cached_property
    def path(self):
        path = re.sub(pattern=self.path_prefix, repl='', string=self.path, flags=re.IGNORECASE)
        # remove path variables
        return re.sub(pattern=r'\{[\w\-]+\}', repl='', string=path)

    def get_operation_id(self):
        """
        Compute an operation ID from the view type and get_operation_id_base method.
        """
        method_name = getattr(self.view, 'action', self.method.lower())
        if self._is_list_view():
            action = 'list'
        elif method_name not in self.method_mapping:
            action = self._to_camel_case(method_name)
        else:
            action = self.method_mapping[self.method.lower()]

        name = self.get_operation_id_base(action)

        return action + name

    def get_operation_id_base(self, action):
        """
        Compute the base part for operation ID from the model, serializer or view name.
        """
        model = getattr(getattr(self.view, 'queryset', None), 'model', None)

        if self.operation_id_base is not None:
            name = self.operation_id_base

        # Try to deduce the ID from the view's model
        elif model is not None:
            name = model.__name__

        # Try with the serializer class name
        elif self.get_serializer() is not None:
            name = self.get_serializer().__class__.__name__
            if name.endswith('Serializer'):
                name = name[:-10]

        # Fallback to the view name
        else:
            name = self.view.__class__.__name__
            if name.endswith('APIView'):
                name = name[:-7]
            elif name.endswith('View'):
                name = name[:-4]

            # Due to camel-casing of classes and `action` being lowercase, apply title in order to find if action truly
            # comes at the end of the name
            if name.endswith(action.title()):  # ListView, UpdateAPIView, ThingDelete ...
                name = name[:-len(action)]

        if action == 'list' and not name.endswith('s'):  # listThings instead of listThing
            name += 's'

        return name

    def get_serializer(self):
        view = self.view

        if not hasattr(view, 'get_serializer'):
            return None

        try:
            return view.get_serializer()
        except Exception:
            return None

    def _to_camel_case(self, snake_str):
        components = snake_str.split('_')
        # We capitalize the first letter of each component except the first one
        # with the 'title' method and join them together.
        return components[0] + ''.join(x.title() for x in components[1:])


================================================
FILE: cookbook/helper/fdc_helper.py
================================================
import json


def get_all_nutrient_types():
    f = open('')  # <--- download the foundation food or any other dataset and retrieve all nutrition ID's from it https://fdc.nal.usda.gov/download-datasets.html
    json_data = json.loads(f.read())

    nutrients = {}
    for food in json_data['FoundationFoods']:
        for entry in food['foodNutrients']:
            nutrients[entry['nutrient']['id']] = {'name': entry['nutrient']['name'], 'unit': entry['nutrient']['unitName']}

    nutrient_ids = list(nutrients.keys())
    nutrient_ids.sort()
    for nid in nutrient_ids:
        print('{', f'value: {nid}, text: "{nutrients[nid]["name"]} [{nutrients[nid]["unit"]}] ({nid})"', '},')


get_all_nutrient_types()


================================================
FILE: cookbook/helper/image_processing.py
================================================
import os
from io import BytesIO

from PIL import Image


def rescale_image_jpeg(image_object, base_width=1020):
    img = Image.open(image_object)
    icc_profile = img.info.get('icc_profile')  # remember color profile to not mess up colors
    width_percent = (base_width / float(img.size[0]))
    height = int((float(img.size[1]) * float(width_percent)))

    img = img.resize((base_width, height), Image.LANCZOS)
    img_bytes = BytesIO()
    img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)

    return img_bytes


def rescale_image_png(image_object, base_width=1020):
    image_object = Image.open(image_object)
    wpercent = (base_width / float(image_object.size[0]))
    hsize = int((float(image_object.size[1]) * float(wpercent)))
    img = image_object.resize((base_width, hsize), Image.LANCZOS)

    im_io = BytesIO()
    img.save(im_io, 'PNG', quality=90)
    return im_io


def get_filetype(name):
    try:
        return os.path.splitext(name)[1]
    except Exception:
        return '.jpeg'


def is_file_type_allowed(filename, image_only=False):
    is_file_allowed = False
    allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css', '.mp4', '.mov']
    allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
    check_list = allowed_image_types
    if not image_only:
        check_list += allowed_file_types

    for file_type in check_list:
        if filename.lower().endswith(file_type):
            is_file_allowed = True

    return is_file_allowed


def strip_image_meta(image_object, file_format):
    image_object = Image.open(image_object)

    data = list(image_object.getdata())
    image_without_exif = Image.new(image_object.mode, image_object.size)
    image_without_exif.putdata(data)

    im_io = BytesIO()
    image_without_exif.save(im_io, file_format)
    return im_io


# TODO this whole file needs proper documentation, refactoring, and testing
# TODO also add env variable to define which images sizes should be compressed
# filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg
# Because it's no longer optional, no reason to return it
def handle_image(request, image_object, filetype):
    try:
        Image.open(image_object).verify()
    except Exception:
        return None

    file_format = None
    if filetype == '.jpeg' or filetype == '.jpg':
        file_format = 'JPEG'
    if filetype == '.png':
        file_format = 'PNG'
    if filetype == '.webp':
        file_format = 'WEBP'

    if (image_object.size / 1000) > 500:  # if larger than 500 kb compress
        if filetype == '.jpeg' or filetype == '.jpg':
            return rescale_image_jpeg(image_object)
        if filetype == '.png':
            return rescale_image_png(image_object)
    else:
        return strip_image_meta(image_object, file_format)

    # TODO webp and gifs bypass the scaling and metadata checks, fix
    return image_object


================================================
FILE: cookbook/helper/ingredient_parser.py
================================================
import re
import string
import unicodedata
from django.db.models import Q

from cookbook.helper.automation_helper import AutomationEngine
from cookbook.models import Food, Ingredient, Unit


class IngredientParser:
    request = None
    ignore_rules = False
    automation = None

    def __init__(self, request, cache_mode=True, ignore_automations=False):
        """
        Initialize ingredient parser
        :param request: request context (to control caching, rule ownership, etc.)
        :param cache_mode: defines if all rules should be loaded on initialization (good when parser is used many times) or if they should be retrieved every time (good when parser is not used many times in a row)
        :param ignore_automations: ignore automation rules, allows to use ingredient parser without database access/request (request can be None)
        """
        self.request = request
        self.ignore_rules = ignore_automations
        if not self.ignore_rules:
            self.automation = AutomationEngine(self.request, use_cache=cache_mode)

    def get_unit(self, unit_name):
        """
        Get or create a unit for given space respecting possible automations
        :param unit_name: string unit
        :return: None if unit passed is invalid, Unit object otherwise
        """
        if not unit_name:
            return None
        if len(unit_name) > 0:
            if not self.ignore_rules:
                unit_name = self.automation.apply_unit_automation(unit_name)

            if unit_obj := Unit.objects.filter(space=self.request.space).filter(Q(name=unit_name) | Q(plural_name=unit_name)).first():
                return unit_obj
            else:
                return Unit.objects.create(space=self.request.space, name=unit_name)
        return None

    def get_food(self, food_name):
        """
        Get or create a food for given space respecting possible automations
        :param food_name: string food
        :return: None if food passed is invalid, Food object otherwise
        """
        if not food_name:
            return None
        if len(food_name) > 0:
            if not self.ignore_rules:
                food_name = self.automation.apply_food_automation(food_name)

            if food_obj := Food.objects.filter(space=self.request.space).filter(Q(name=food_name) | Q(plural_name=food_name)).first():
                return food_obj
            else:
                return Food.objects.create(space=self.request.space, name=food_name)
        return None

    def parse_fraction(self, x):
        if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
            frac_split = unicodedata.decomposition(x[-1:]).split()
            return (float((frac_split[1]).replace('003', ''))
                    / float((frac_split[3]).replace('003', '')))
        else:
            frac_split = x.split('/')
            if not len(frac_split) == 2:
                raise ValueError
            try:
                return int(frac_split[0]) / int(frac_split[1])
            except ZeroDivisionError:
                raise ValueError

    def parse_amount(self, x):
        amount = 0
        unit = None
        note = ''
        if x.strip() == '':
            return amount, unit, note

        did_check_frac = False
        end = 0
        while (end < len(x) and (x[end] in string.digits
                                 or (
            (x[end] == '.' or x[end] == ',' or x[end] == '/')
            and end + 1 < len(x)
            and x[end + 1] in string.digits
        ))):
            end += 1
        if end > 0:
            if "/" in x[:end]:
                amount = self.parse_fraction(x[:end])
            else:
                amount = float(x[:end].replace(',', '.'))
        else:
            amount = self.parse_fraction(x[0])
            end += 1
            did_check_frac = True
        if end < len(x):
            if did_check_frac:
                unit = x[end:]
            else:
                try:
                    amount += self.parse_fraction(x[end])
                    unit = x[end + 1:]
                except ValueError:
                    unit = x[end:]

        if unit is not None and unit.strip() == '':
            unit = None

        if unit is not None and (unit.startswith('(') or unit.startswith(
                '-')):  # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
            unit = None
            note = x
        return amount, unit, note

    def parse_food_with_comma(self, tokens):
        food = ''
        note = ''
        start = 0
        # search for first occurrence of an argument ending in a comma
        while start < len(tokens) and not tokens[start].endswith((',', ';', ':')):
            start += 1
        if start == len(tokens):
            # no token ending in a comma found -> use everything as food
            food = ' '.join(tokens)
        else:
            food = ' '.join(tokens[:start + 1])[:-1]
            note = ' '.join(tokens[start + 1:])
        return food, note

    def parse_food(self, tokens):
        food = ''
        note = ''
        if tokens[-1].endswith(')'):
            # Check if the matching opening bracket is in the same token
            if (not tokens[-1].startswith('(')) and ('(' in tokens[-1]):
                return self.parse_food_with_comma(tokens)
            # last argument ends with closing bracket -> look for opening bracket
            start = len(tokens) - 1
            while not tokens[start].startswith('(') and not start == 0:
                start -= 1
            if start == 0:
                # the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit)  # noqa: E501
                raise ValueError
            elif start < 0:
                # no opening bracket anywhere -> just ignore the last bracket
                food, note = self.parse_food_with_comma(tokens)
            else:
                # opening bracket found -> split in food and note, remove brackets from note  # noqa: E501
                note = ' '.join(tokens[start:])[1:-1]
                food = ' '.join(tokens[:start])
        else:
            food, note = self.parse_food_with_comma(tokens)
        return food, note

    def parse(self, ingredient):
        """
        Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ...
        :param ingredient: string ingredient
        :return: amount, unit (can be None), food, note (can be empty)
        """
        # initialize default values
        amount = 0
        unit = None
        food = ''
        note = ''
        unit_note = ''

        ingredient = ingredient.strip()

        if len(ingredient) == 0:
            raise ValueError('string to parse cannot be empty')

        if len(ingredient) > 512:
            raise ValueError('cannot parse ingredients with more than 512 characters')

        # remove leading commas, dots and other symbols that typically do not occur at the start of an ingredient string
        ingredient = re.sub(r"^[,.\-_=+#*|\\/]+", "", ingredient)

        # some people/languages put amount and unit at the end of the ingredient string
        # if something like this is detected move it to the beginning so the parser can handle it
        if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient):
            match = re.search(r'[1-9](\d)*\s*([^\W\d_])+', ingredient)
            ingredient = ingredient[match.start():match.end()] + ' ' + ingredient.replace(ingredient[match.start():match.end()], '')

        # if the string contains parenthesis early on remove it and place it at the end
        # because its likely some kind of note
        if re.match('(.){1,6}\\s\\((.[^\\(\\)])+\\)\\s', ingredient):
            match = re.search('\\((.[^\\(])+\\)', ingredient)
            ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()]

        # leading spaces before commas result in extra tokens, clean them out
        ingredient = ingredient.replace(' ,', ',')

        # handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description
        # "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)"
        ingredient = re.sub("^(\\d+|\\d+[\\.,]\\d+) - (\\d+|\\d+[\\.,]\\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)

        # if amount and unit are connected add space in between
        if re.match('([0-9])+([A-z])+\\s', ingredient):
            ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)

        if not self.ignore_rules:
            ingredient = self.automation.apply_transpose_automation(ingredient)

        tokens = ingredient.split()  # split at each space into tokens
        if len(tokens) == 1:
            # there only is one argument, that must be the food
            food = tokens[0]
        else:
            try:
                # try to parse first argument as amount
                amount, unit, unit_note = self.parse_amount(tokens[0])
                # only try to parse second argument as amount if there are at least
                # three arguments if it already has a unit there can't be
                # a fraction for the amount
                if len(tokens) > 2:
                    never_unit_applied = False
                    if not self.ignore_rules:
                        tokens, never_unit_applied = self.automation.apply_never_unit_automation(tokens)

                    if never_unit_applied:
                        unit = tokens[1]
                        food, note = self.parse_food(tokens[2:])
                    else:
                        try:
                            if unit is not None:
                                # a unit is already found, no need to try the second argument for a fraction
                                # probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
                                raise ValueError
                            # try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
                            if tokens[1]:
                                amount += self.parse_fraction(tokens[1])
                            # assume that units can't end with a comma
                            if len(tokens) > 3 and not tokens[2].endswith(','):
                                # try to use third argument as unit and everything else as food, use everything as food if it fails
                                try:
                                    food, note = self.parse_food(tokens[3:])
                                    unit = tokens[2]
                                except ValueError:
                                    food, note = self.parse_food(tokens[2:])
                            else:
                                food, note = self.parse_food(tokens[2:])
                        except ValueError:
                            # assume that units can't end with a comma
                            if not tokens[1].endswith(','):
                                # try to use second argument as unit and everything else as food, use everything as food if it fails
                                try:
                                    food, note = self.parse_food(tokens[2:])
                                    if unit is None:
                                        unit = tokens[1]
                                    else:
                                        note = tokens[1]
                                except ValueError:
                                    food, note = self.parse_food(tokens[1:])
                            else:
                                food, note = self.parse_food(tokens[1:])
                else:
                    # only two arguments, first one is the amount
                    # which means this is the food
                    food = tokens[1]
            except ValueError:
                try:
                    # can't parse first argument as amount
                    # -> no unit -> parse everything as food
                    food, note = self.parse_food(tokens)
                except ValueError:
                    food = ' '.join(tokens[1:])

        if unit_note not in note:
            note += ' ' + unit_note

        if unit and not self.ignore_rules:
            unit = self.automation.apply_unit_automation(unit)

        if food and not self.ignore_rules:
            food = self.automation.apply_food_automation(food)

        if len(food) > Food._meta.get_field('name').max_length:  # test if food name is to long
            # try splitting it at a space and taking only the first arg
            if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:
                note = ' '.join(food.split()[1:]) + ' ' + note
                food = food.split()[0]
            else:
                note = food + ' ' + note
                food = food[:Food._meta.get_field('name').max_length]

        if len(food.strip()) == 0:
            raise ValueError(f'Error parsing string {ingredient}, food cannot be empty')

        return amount, unit, food, note[:Ingredient._meta.get_field('note').max_length].strip()

    def parse_as_ingredient(self, text):
        """
        Parse ingredient string into ingredient object with nested food information
        :param text: ingredient string
        :return: ingredient object
        """
        amount, unit, food, note = self.parse(text)

        ingredient = Ingredient(
            amount=amount,
            unit=None,
            food=None,
            space=self.request.space,
            note=note,
            original_text=text
        )
        if food:
            ingredient.food = self.get_food(food)

        if unit:
            ingredient.unit = self.get_unit(unit)

        return ingredient

================================================
FILE: cookbook/helper/mdx_attributes.py
================================================
import markdown
from markdown.treeprocessors import Treeprocessor


class StyleTreeprocessor(Treeprocessor):

    def run_processor(self, node):
        for child in node:
            if child.tag == "table":
                child.set("class", "markdown-table")
            if child.tag == "th" or child.tag == "td":
                child.set("class", "markdown-table-cell")
            if child.tag == "img":
                child.set("class", "img-fluid")
            self.run_processor(child)
        return node

    def run(self, root):
        self.run_processor(root)
        return root


class MarkdownFormatExtension(markdown.Extension):
    # md_ globals deprecated - see here:
    def extendMarkdown(self, md):
        md.treeprocessors.register(
            StyleTreeprocessor(),
            'StyleTreeprocessor',
            10
        )


================================================
FILE: cookbook/helper/mdx_urlize.py
================================================
"""
A more liberal autolinker

Inspired by Django's urlize function.

Positive examples:

>>> import markdown
>>> md = markdown.Markdown(extensions=['urlize'])

>>> md.convert('http://example.com/')
u'<p><a href="http://example.com/">http://example.com/</a></p>'

>>> md.convert('go to http://example.com')
u'<p>go to <a href="http://example.com">http://example.com</a></p>'

>>> md.convert('example.com')
u'<p><a href="http://example.com">example.com</a></p>'

>>> md.convert('example.net')
u'<p><a href="http://example.net">example.net</a></p>'

>>> md.convert('www.example.us')
u'<p><a href="http://www.example.us">www.example.us</a></p>'

>>> md.convert('(www.example.us/path/?name=val)')
u'<p>(<a href="http://www.example.us/path/?name=val">www.example.us/path/?name=val</a>)</p>'

>>> md.convert('go to <http://example.com> now!')
u'<p>go to <a href="http://example.com">http://example.com</a> now!</p>'

Negative examples:

>>> md.convert('del.icio.us')
u'<p>del.icio.us</p>'

"""
from xml.etree.ElementTree import Element

import markdown

# Global Vars
URLIZE_RE = '(%s)' % '|'.join([
    r'<(?:f|ht)tps?://[^>]*>',
    r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
    r'\bwww\.[^)<>\s]+[^.,)<>\s]',
    r'[^(<\s]+\.(?:com|net|org)\b',
])


class UrlizePattern(markdown.inlinepatterns.Pattern):
    """ Return a link Element given an autolink (`http://example/com`). """

    def handleMatch(self, m):
        url = m.group(2)

        if url.startswith('<'):
            url = url[1:-1]

        text = url

        if not url.split('://')[0] in ('http', 'https', 'ftp'):
            if '@' in url and '/' not in url:
                url = 'mailto:' + url
            else:
                url = 'http://' + url

        el = Element("a")
        el.set('href', url)
        el.text = markdown.util.AtomicString(text)
        return el


class UrlizeExtension(markdown.Extension):
    """ Urlize Extension for Python-Markdown. """

    def extendMarkdown(self, md):
        """ Replace autolink with UrlizePattern """
        md.inlinePatterns.register(UrlizePattern(URLIZE_RE, md), 'autolink', 120)


def makeExtension(*args, **kwargs):
    return UrlizeExtension(*args, **kwargs)


if __name__ == "__main__":
    import doctest

    doctest.testmod()


================================================
FILE: cookbook/helper/open_data_importer.py
================================================
import traceback
from collections import defaultdict
from decimal import Decimal

from cookbook.models import (Food, FoodProperty, Property, PropertyType, Supermarket,
                             SupermarketCategory, SupermarketCategoryRelation, Unit, UnitConversion)
import re

from recipes.settings import DEBUG


class OpenDataImportResponse:
    total_created = 0
    total_updated = 0
    total_untouched = 0
    total_errored = 0

    def to_dict(self):
        return {'total_created': self.total_created, 'total_updated': self.total_updated, 'total_untouched': self.total_untouched, 'total_errored': self.total_errored}


class OpenDataImporter:
    request = None
    data = {}
    slug_id_cache = {}
    update_existing = False
    use_metric = True

    def __init__(self, request, data, update_existing=False, use_metric=True):
        self.request = request
        self.data = data
        self.update_existin
Download .txt
gitextract_yzw4jm7n/

├── .coveragerc
├── .devcontainer/
│   ├── Dockerfile
│   └── devcontainer.json
├── .dockerignore
├── .flake8
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md.bak
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── doc_issue.yml
│   │   ├── feature_request.md.bak
│   │   ├── feature_request.yml
│   │   ├── help-request.md.bak
│   │   ├── help_request.yml
│   │   ├── url_import.md.bak
│   │   └── website_import.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-docker.yml
│       ├── ci.yml
│       ├── codeql-analysis.yml
│       └── docs.yml
├── .gitignore
├── .idea/
│   ├── codeStyles/
│   │   └── codeStyleConfig.xml
│   ├── dictionaries/
│   │   ├── vaben.xml
│   │   └── vabene1111_PC.xml
│   ├── inspectionProfiles/
│   │   └── profiles_settings.xml
│   ├── modules.xml
│   ├── prettier.xml
│   ├── recipes.iml
│   └── watcherTasks.xml
├── .prettierignore
├── .prettierrc
├── .vscode/
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── CONTRIBUTERS.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── SECURITY.md
├── boot.sh
├── cookbook/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── connectors/
│   │   ├── __init__.py
│   │   ├── connector.py
│   │   ├── connector_manager.py
│   │   └── homeassistant.py
│   ├── fixtures/
│   │   └── german_example_tags.json
│   ├── forms.py
│   ├── helper/
│   │   ├── AllAuthCustomAdapter.py
│   │   ├── CustomStorageClass.py
│   │   ├── HelperFunctions.py
│   │   ├── __init__.py
│   │   ├── ai_helper.py
│   │   ├── automation_helper.py
│   │   ├── batch_edit_helper.py
│   │   ├── cache_helper.py
│   │   ├── context_processors.py
│   │   ├── cooklang_parser.py
│   │   ├── drf_spectacular_hooks.py
│   │   ├── fdc_helper.py
│   │   ├── image_processing.py
│   │   ├── ingredient_parser.py
│   │   ├── mdx_attributes.py
│   │   ├── mdx_urlize.py
│   │   ├── open_data_importer.py
│   │   ├── permission_config.py
│   │   ├── permission_helper.py
│   │   ├── property_helper.py
│   │   ├── recipe_search.py
│   │   ├── recipe_url_import.py
│   │   ├── scope_middleware.py
│   │   ├── shopping_helper.py
│   │   ├── template_helper.py
│   │   └── unit_conversion_helper.py
│   ├── integration/
│   │   ├── cheftap.py
│   │   ├── chowdown.py
│   │   ├── cookbookapp.py
│   │   ├── cooklang.py
│   │   ├── cookmate.py
│   │   ├── copymethat.py
│   │   ├── default.py
│   │   ├── domestica.py
│   │   ├── gourmet.py
│   │   ├── integration.py
│   │   ├── mealie.py
│   │   ├── mealie1.py
│   │   ├── mealmaster.py
│   │   ├── melarecipes.py
│   │   ├── nextcloud_cookbook.py
│   │   ├── openeats.py
│   │   ├── paprika.py
│   │   ├── pdfexport.py
│   │   ├── pepperplate.py
│   │   ├── plantoeat.py
│   │   ├── recettetek.py
│   │   ├── recipekeeper.py
│   │   ├── recipesage.py
│   │   ├── rezeptsuitede.py
│   │   ├── rezkonv.py
│   │   └── saffron.py
│   ├── locale/
│   │   ├── ar/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── bg/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ca/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── da/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── el/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── es/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── he/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hu_HU/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hy/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── id/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ko/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── lv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nb_NO/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nn/
│   │   │   └── LC_MESSAGES/
│   │   │       └── django.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── rn/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ro/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── tr/
│   │   │   ├── LC_MESSAGES/
│   │   │   │   ├── django.mo
│   │   │   │   └── django.po
│   │   │   └── id/
│   │   │       └── LC_MESSAGES/
│   │   │           └── django.po
│   │   ├── uk/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── vi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── zh_CN/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   └── zh_Hant/
│   │       └── LC_MESSAGES/
│   │           ├── django.mo
│   │           └── django.po
│   ├── management/
│   │   └── commands/
│   │       ├── export.py
│   │       ├── fix_duplicate_properties.py
│   │       ├── import.py
│   │       ├── rebuildindex.py
│   │       └── seed_basic_data.py
│   ├── managers.py
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   ├── 0001_squashed_0227_space_ai_default_provider_and_more.py
│   │   ├── 0002_auto_20191119_2035.py
│   │   ├── 0003_enable_pgtrm.py
│   │   ├── 0004_storage_created_by.py
│   │   ├── 0005_recipebook_recipebookentry.py
│   │   ├── 0006_recipe_image.py
│   │   ├── 0007_auto_20191226_0852.py
│   │   ├── 0008_mealplan.py
│   │   ├── 0009_auto_20200130_1056.py
│   │   ├── 0010_auto_20200130_1059.py
│   │   ├── 0011_remove_recipeingredients_unit.py
│   │   ├── 0012_auto_20200130_1116.py
│   │   ├── 0013_userpreference.py
│   │   ├── 0014_auto_20200213_2332.py
│   │   ├── 0015_auto_20200213_2334.py
│   │   ├── 0016_auto_20200213_2335.py
│   │   ├── 0017_auto_20200216_2257.py
│   │   ├── 0018_auto_20200216_2303.py
│   │   ├── 0019_ingredient.py
│   │   ├── 0020_recipeingredient_ingredient.py
│   │   ├── 0021_auto_20200216_2309.py
│   │   ├── 0022_remove_recipeingredient_name.py
│   │   ├── 0023_auto_20200216_2311.py
│   │   ├── 0024_auto_20200216_2313.py
│   │   ├── 0025_userpreference_nav_color.py
│   │   ├── 0026_auto_20200219_1605.py
│   │   ├── 0027_ingredient_recipe.py
│   │   ├── 0028_auto_20200317_1901.py
│   │   ├── 0029_auto_20200317_1901.py
│   │   ├── 0030_recipeingredient_note.py
│   │   ├── 0031_auto_20200407_1841.py
│   │   ├── 0032_userpreference_default_unit.py
│   │   ├── 0033_userpreference_default_page.py
│   │   ├── 0034_auto_20200426_1614.py
│   │   ├── 0035_auto_20200427_1637.py
│   │   ├── 0036_auto_20200427_1800.py
│   │   ├── 0037_userpreference_search_style.py
│   │   ├── 0038_auto_20200502_1259.py
│   │   ├── 0039_recipebook_shared.py
│   │   ├── 0040_auto_20200502_1433.py
│   │   ├── 0041_auto_20200502_1446.py
│   │   ├── 0042_cooklog.py
│   │   ├── 0043_auto_20200507_2302.py
│   │   ├── 0044_viewlog.py
│   │   ├── 0045_userpreference_show_recent.py
│   │   ├── 0046_auto_20200602_1133.py
│   │   ├── 0047_auto_20200602_1133.py
│   │   ├── 0048_auto_20200602_1140.py
│   │   ├── 0049_mealtype_created_by.py
│   │   ├── 0050_auto_20200611_1509.py
│   │   ├── 0051_auto_20200611_1518.py
│   │   ├── 0052_userpreference_ingredient_decimals.py
│   │   ├── 0053_auto_20200611_2217.py
│   │   ├── 0054_sharelink.py
│   │   ├── 0055_auto_20200616_1236.py
│   │   ├── 0056_auto_20200625_2118.py
│   │   ├── 0056_auto_20200625_2157.py
│   │   ├── 0057_auto_20200625_2127.py
│   │   ├── 0058_auto_20200625_2128.py
│   │   ├── 0059_auto_20200625_2137.py
│   │   ├── 0060_auto_20200625_2144.py
│   │   ├── 0061_merge_20200625_2209.py
│   │   ├── 0062_auto_20200625_2219.py
│   │   ├── 0063_auto_20200625_2230.py
│   │   ├── 0064_auto_20200625_2329.py
│   │   ├── 0065_auto_20200626_1444.py
│   │   ├── 0066_auto_20200626_1455.py
│   │   ├── 0067_auto_20200629_1508.py
│   │   ├── 0068_auto_20200629_2127.py
│   │   ├── 0069_auto_20200629_2134.py
│   │   ├── 0070_auto_20200701_2007.py
│   │   ├── 0071_auto_20200701_2048.py
│   │   ├── 0072_step_show_as_header.py
│   │   ├── 0073_auto_20200708_2311.py
│   │   ├── 0074_remove_keyword_created_by.py
│   │   ├── 0075_shoppinglist_shoppinglistentry_shoppinglistrecipe.py
│   │   ├── 0076_shoppinglist_entries.py
│   │   ├── 0077_invitelink.py
│   │   ├── 0078_invitelink_used_by.py
│   │   ├── 0079_invitelink_group.py
│   │   ├── 0080_auto_20200921_2331.py
│   │   ├── 0081_auto_20200921_2349.py
│   │   ├── 0082_auto_20200922_1143.py
│   │   ├── 0083_space.py
│   │   ├── 0084_auto_20200922_1233.py
│   │   ├── 0085_auto_20200922_1235.py
│   │   ├── 0086_auto_20200929_1143.py
│   │   ├── 0087_auto_20200929_1152.py
│   │   ├── 0088_shoppinglist_finished.py
│   │   ├── 0089_auto_20201117_2222.py
│   │   ├── 0090_auto_20201214_1359.py
│   │   ├── 0091_auto_20201226_1551.py
│   │   ├── 0092_recipe_servings.py
│   │   ├── 0093_auto_20201231_1236.py
│   │   ├── 0094_auto_20201231_1238.py
│   │   ├── 0095_auto_20210107_1804.py
│   │   ├── 0096_auto_20210109_2044.py
│   │   ├── 0097_auto_20210113_1315.py
│   │   ├── 0098_auto_20210113_1320.py
│   │   ├── 0099_auto_20210113_1518.py
│   │   ├── 0100_recipe_servings_text.py
│   │   ├── 0101_storage_path.py
│   │   ├── 0102_auto_20210125_1147.py
│   │   ├── 0103_food_ignore_shopping.py
│   │   ├── 0104_auto_20210125_2133.py
│   │   ├── 0105_auto_20210126_1604.py
│   │   ├── 0106_shoppinglist_supermarket.py
│   │   ├── 0107_auto_20210128_1535.py
│   │   ├── 0108_auto_20210219_1410.py
│   │   ├── 0109_auto_20210221_1204.py
│   │   ├── 0110_auto_20210221_1406.py
│   │   ├── 0111_space_created_by.py
│   │   ├── 0112_remove_synclog_space.py
│   │   ├── 0113_auto_20210317_2017.py
│   │   ├── 0114_importlog.py
│   │   ├── 0115_telegrambot.py
│   │   ├── 0116_auto_20210319_0012.py
│   │   ├── 0117_space_max_recipes.py
│   │   ├── 0118_auto_20210406_1805.py
│   │   ├── 0119_auto_20210411_2101.py
│   │   ├── 0120_bookmarklet.py
│   │   ├── 0121_auto_20210518_1638.py
│   │   ├── 0122_auto_20210527_1712.py
│   │   ├── 0123_invitelink_email.py
│   │   ├── 0124_alter_userpreference_theme.py
│   │   ├── 0125_space_demo.py
│   │   ├── 0126_alter_userpreference_theme.py
│   │   ├── 0127_remove_invitelink_username.py
│   │   ├── 0128_userfile.py
│   │   ├── 0129_auto_20210608_1233.py
│   │   ├── 0130_alter_userfile_file_size_kb.py
│   │   ├── 0131_auto_20210608_1929.py
│   │   ├── 0132_sharelink_request_count.py
│   │   ├── 0133_sharelink_abuse_blocked.py
│   │   ├── 0134_space_allow_sharing.py
│   │   ├── 0135_auto_20210615_2210.py
│   │   ├── 0136_auto_20210617_1343.py
│   │   ├── 0137_auto_20210617_1501.py
│   │   ├── 0138_auto_20210617_1602.py
│   │   ├── 0139_space_created_at.py
│   │   ├── 0140_userpreference_created_at.py
│   │   ├── 0141_auto_20210713_1042.py
│   │   ├── 0142_alter_userpreference_search_style.py
│   │   ├── 0143_build_full_text_index.py
│   │   ├── 0144_create_searchfields.py
│   │   ├── 0145_alter_userpreference_search_style.py
│   │   ├── 0146_alter_userpreference_use_fractions.py
│   │   ├── 0147_keyword_to_tree.py
│   │   ├── 0148_auto_20210813_1829.py
│   │   ├── 0149_fix_leading_trailing_spaces.py
│   │   ├── 0150_food_to_tree.py
│   │   ├── 0151_auto_20210915_1037.py
│   │   ├── 0152_automation.py
│   │   ├── 0153_auto_20210915_2327.py
│   │   ├── 0154_auto_20210922_1705.py
│   │   ├── 0155_mealtype_default.py
│   │   ├── 0156_searchpreference_trigram_threshold.py
│   │   ├── 0157_alter_searchpreference_trigram.py
│   │   ├── 0158_userpreference_use_kj.py
│   │   ├── 0159_add_shoppinglistentry_fields.py
│   │   ├── 0160_delete_shoppinglist_orphans.py
│   │   ├── 0161_alter_shoppinglistentry_food.py
│   │   ├── 0162_userpreference_csv_delim.py
│   │   ├── 0163_auto_20220105_0758.py
│   │   ├── 0164_space_show_facet_count.py
│   │   ├── 0165_remove_step_type.py
│   │   ├── 0166_alter_userpreference_shopping_add_onhand.py
│   │   ├── 0167_userpreference_left_handed.py
│   │   ├── 0168_add_unit_searchfields.py
│   │   ├── 0169_exportlog.py
│   │   ├── 0170_auto_20220207_1848.py
│   │   ├── 0171_alter_searchpreference_trigram_threshold.py
│   │   ├── 0172_ingredient_original_text.py
│   │   ├── 0173_recipe_source_url.py
│   │   ├── 0174_alter_food_substitute_userspace.py
│   │   ├── 0175_remove_userpreference_space.py
│   │   ├── 0176_alter_searchpreference_icontains_and_more.py
│   │   ├── 0177_recipe_show_ingredient_overview.py
│   │   ├── 0178_remove_userpreference_search_style_and_more.py
│   │   ├── 0179_recipe_private_recipe_shared.py
│   │   ├── 0180_invitelink_reusable.py
│   │   ├── 0181_space_image.py
│   │   ├── 0182_userpreference_image.py
│   │   ├── 0183_alter_space_image.py
│   │   ├── 0184_alter_userpreference_image.py
│   │   ├── 0185_food_plural_name_ingredient_always_use_plural_food_and_more.py
│   │   ├── 0186_automation_order_alter_automation_type.py
│   │   ├── 0187_alter_space_use_plural.py
│   │   ├── 0188_space_no_sharing_limit.py
│   │   ├── 0189_property_propertytype_unitconversion_food_fdc_id_and_more.py
│   │   ├── 0190_auto_20230525_1506.py
│   │   ├── 0191_foodproperty_property_import_food_id_and_more.py
│   │   ├── 0192_food_food_unique_open_data_slug_per_space_and_more.py
│   │   ├── 0193_space_internal_note.py
│   │   ├── 0194_alter_food_properties_food_amount.py
│   │   ├── 0195_invitelink_internal_note_userspace_internal_note_and_more.py
│   │   ├── 0196_food_url.py
│   │   ├── 0197_step_show_ingredients_table_and_more.py
│   │   ├── 0198_propertytype_order.py
│   │   ├── 0199_alter_propertytype_options_alter_automation_type_and_more.py
│   │   ├── 0200_alter_propertytype_options_remove_keyword_icon_and_more.py
│   │   ├── 0201_rename_date_mealplan_from_date_mealplan_to_date.py
│   │   ├── 0202_remove_space_show_facet_count.py
│   │   ├── 0203_alter_unique_contstraints.py
│   │   ├── 0204_propertytype_fdc_id.py
│   │   ├── 0205_alter_food_fdc_id_alter_propertytype_fdc_id.py
│   │   ├── 0206_rename_sticky_navbar_userpreference_nav_sticky_and_more.py
│   │   ├── 0207_space_logo_color_128_space_logo_color_144_and_more.py
│   │   ├── 0208_space_app_name_userpreference_max_owned_spaces.py
│   │   ├── 0209_remove_space_use_plural.py
│   │   ├── 0210_shoppinglistentry_updated_at.py
│   │   ├── 0211_recipebook_order.py
│   │   ├── 0212_alter_property_property_amount.py
│   │   ├── 0213_remove_property_property_unique_import_food_per_space_and_more.py
│   │   ├── 0214_cooklog_comment_cooklog_updated_at_and_more.py
│   │   ├── 0215_connectorconfig.py
│   │   ├── 0216_delete_shoppinglist.py
│   │   ├── 0217_alter_userpreference_default_page.py
│   │   ├── 0218_alter_mealplan_from_date_alter_mealplan_to_date.py
│   │   ├── 0219_connectorconfig_supports_description_field.py
│   │   ├── 0220_shoppinglistrecipe_created_by_and_more.py
│   │   ├── 0221_migrate_shoppinglistrecipe_space_created_by.py
│   │   ├── 0222_alter_shoppinglistrecipe_created_by_and_more.py
│   │   ├── 0223_auto_20250831_1111.py
│   │   ├── 0224_space_ai_credits_balance_space_ai_credits_monthly_and_more.py
│   │   ├── 0225_space_ai_enabled.py
│   │   ├── 0226_aiprovider_log_credit_cost_and_more.py
│   │   ├── 0227_space_ai_default_provider_and_more.py
│   │   ├── 0228_space_space_setup_completed.py
│   │   ├── 0229_alter_ailog_options_alter_aiprovider_options_and_more.py
│   │   ├── 0230_auto_20250925_2056.py
│   │   ├── 0231_alter_aiprovider_options_alter_automation_options_and_more.py
│   │   ├── 0232_shoppinglist.py
│   │   ├── 0233_food_shopping_lists_shoppinglistentry_shopping_lists_and_more.py
│   │   ├── 0234_alter_shoppinglist_options_and_more.py
│   │   ├── 0235_recipe_diameter_recipe_diameter_text.py
│   │   ├── 0236_household_userspace_household_inventorylocation_and_more.py
│   │   ├── 0237_remove_mealtype_mt_unique_name_per_space_and_more.py
│   │   ├── 0238_auto_20260312_1920.py
│   │   └── __init__.py
│   ├── models.py
│   ├── provider/
│   │   ├── __init__.py
│   │   ├── dropbox.py
│   │   ├── local.py
│   │   ├── nextcloud.py
│   │   └── provider.py
│   ├── serializer.py
│   ├── signals.py
│   ├── static/
│   │   ├── custom/
│   │   │   └── css/
│   │   │       └── markdown_blockquote.css
│   │   └── pdfjs/
│   │       ├── LICENSE
│   │       └── web/
│   │           ├── cmaps/
│   │           │   ├── 78-EUC-H.bcmap
│   │           │   ├── 78-EUC-V.bcmap
│   │           │   ├── 78-H.bcmap
│   │           │   ├── 78-RKSJ-H.bcmap
│   │           │   ├── 78-RKSJ-V.bcmap
│   │           │   ├── 78-V.bcmap
│   │           │   ├── 78ms-RKSJ-H.bcmap
│   │           │   ├── 78ms-RKSJ-V.bcmap
│   │           │   ├── 83pv-RKSJ-H.bcmap
│   │           │   ├── 90ms-RKSJ-H.bcmap
│   │           │   ├── 90ms-RKSJ-V.bcmap
│   │           │   ├── 90msp-RKSJ-H.bcmap
│   │           │   ├── 90msp-RKSJ-V.bcmap
│   │           │   ├── 90pv-RKSJ-H.bcmap
│   │           │   ├── 90pv-RKSJ-V.bcmap
│   │           │   ├── Add-H.bcmap
│   │           │   ├── Add-RKSJ-H.bcmap
│   │           │   ├── Add-RKSJ-V.bcmap
│   │           │   ├── Add-V.bcmap
│   │           │   ├── Adobe-CNS1-0.bcmap
│   │           │   ├── Adobe-CNS1-1.bcmap
│   │           │   ├── Adobe-CNS1-2.bcmap
│   │           │   ├── Adobe-CNS1-3.bcmap
│   │           │   ├── Adobe-CNS1-4.bcmap
│   │           │   ├── Adobe-CNS1-5.bcmap
│   │           │   ├── Adobe-CNS1-6.bcmap
│   │           │   ├── Adobe-CNS1-UCS2.bcmap
│   │           │   ├── Adobe-GB1-0.bcmap
│   │           │   ├── Adobe-GB1-1.bcmap
│   │           │   ├── Adobe-GB1-2.bcmap
│   │           │   ├── Adobe-GB1-3.bcmap
│   │           │   ├── Adobe-GB1-4.bcmap
│   │           │   ├── Adobe-GB1-5.bcmap
│   │           │   ├── Adobe-GB1-UCS2.bcmap
│   │           │   ├── Adobe-Japan1-0.bcmap
│   │           │   ├── Adobe-Japan1-1.bcmap
│   │           │   ├── Adobe-Japan1-2.bcmap
│   │           │   ├── Adobe-Japan1-3.bcmap
│   │           │   ├── Adobe-Japan1-4.bcmap
│   │           │   ├── Adobe-Japan1-5.bcmap
│   │           │   ├── Adobe-Japan1-6.bcmap
│   │           │   ├── Adobe-Japan1-UCS2.bcmap
│   │           │   ├── Adobe-Korea1-0.bcmap
│   │           │   ├── Adobe-Korea1-1.bcmap
│   │           │   ├── Adobe-Korea1-2.bcmap
│   │           │   ├── Adobe-Korea1-UCS2.bcmap
│   │           │   ├── B5-H.bcmap
│   │           │   ├── B5-V.bcmap
│   │           │   ├── B5pc-H.bcmap
│   │           │   ├── B5pc-V.bcmap
│   │           │   ├── CNS-EUC-H.bcmap
│   │           │   ├── CNS-EUC-V.bcmap
│   │           │   ├── CNS1-H.bcmap
│   │           │   ├── CNS1-V.bcmap
│   │           │   ├── CNS2-H.bcmap
│   │           │   ├── CNS2-V.bcmap
│   │           │   ├── ETHK-B5-H.bcmap
│   │           │   ├── ETHK-B5-V.bcmap
│   │           │   ├── ETen-B5-H.bcmap
│   │           │   ├── ETen-B5-V.bcmap
│   │           │   ├── ETenms-B5-H.bcmap
│   │           │   ├── ETenms-B5-V.bcmap
│   │           │   ├── EUC-H.bcmap
│   │           │   ├── EUC-V.bcmap
│   │           │   ├── Ext-H.bcmap
│   │           │   ├── Ext-RKSJ-H.bcmap
│   │           │   ├── Ext-RKSJ-V.bcmap
│   │           │   ├── Ext-V.bcmap
│   │           │   ├── GB-EUC-H.bcmap
│   │           │   ├── GB-EUC-V.bcmap
│   │           │   ├── GB-H.bcmap
│   │           │   ├── GB-V.bcmap
│   │           │   ├── GBK-EUC-H.bcmap
│   │           │   ├── GBK-EUC-V.bcmap
│   │           │   ├── GBK2K-H.bcmap
│   │           │   ├── GBK2K-V.bcmap
│   │           │   ├── GBKp-EUC-H.bcmap
│   │           │   ├── GBKp-EUC-V.bcmap
│   │           │   ├── GBT-EUC-H.bcmap
│   │           │   ├── GBT-EUC-V.bcmap
│   │           │   ├── GBT-H.bcmap
│   │           │   ├── GBT-V.bcmap
│   │           │   ├── GBTpc-EUC-H.bcmap
│   │           │   ├── GBTpc-EUC-V.bcmap
│   │           │   ├── GBpc-EUC-H.bcmap
│   │           │   ├── GBpc-EUC-V.bcmap
│   │           │   ├── H.bcmap
│   │           │   ├── HKdla-B5-H.bcmap
│   │           │   ├── HKdla-B5-V.bcmap
│   │           │   ├── HKdlb-B5-H.bcmap
│   │           │   ├── HKdlb-B5-V.bcmap
│   │           │   ├── HKgccs-B5-H.bcmap
│   │           │   ├── HKgccs-B5-V.bcmap
│   │           │   ├── HKm314-B5-H.bcmap
│   │           │   ├── HKm314-B5-V.bcmap
│   │           │   ├── HKm471-B5-H.bcmap
│   │           │   ├── HKm471-B5-V.bcmap
│   │           │   ├── HKscs-B5-H.bcmap
│   │           │   ├── HKscs-B5-V.bcmap
│   │           │   ├── Hankaku.bcmap
│   │           │   ├── Hiragana.bcmap
│   │           │   ├── KSC-EUC-H.bcmap
│   │           │   ├── KSC-EUC-V.bcmap
│   │           │   ├── KSC-H.bcmap
│   │           │   ├── KSC-Johab-H.bcmap
│   │           │   ├── KSC-Johab-V.bcmap
│   │           │   ├── KSC-V.bcmap
│   │           │   ├── KSCms-UHC-H.bcmap
│   │           │   ├── KSCms-UHC-HW-H.bcmap
│   │           │   ├── KSCms-UHC-HW-V.bcmap
│   │           │   ├── KSCms-UHC-V.bcmap
│   │           │   ├── KSCpc-EUC-H.bcmap
│   │           │   ├── KSCpc-EUC-V.bcmap
│   │           │   ├── Katakana.bcmap
│   │           │   ├── LICENSE
│   │           │   ├── NWP-H.bcmap
│   │           │   ├── NWP-V.bcmap
│   │           │   ├── RKSJ-H.bcmap
│   │           │   ├── RKSJ-V.bcmap
│   │           │   ├── Roman.bcmap
│   │           │   ├── UniCNS-UCS2-H.bcmap
│   │           │   ├── UniCNS-UCS2-V.bcmap
│   │           │   ├── UniCNS-UTF16-H.bcmap
│   │           │   ├── UniCNS-UTF16-V.bcmap
│   │           │   ├── UniCNS-UTF32-H.bcmap
│   │           │   ├── UniCNS-UTF32-V.bcmap
│   │           │   ├── UniCNS-UTF8-H.bcmap
│   │           │   ├── UniCNS-UTF8-V.bcmap
│   │           │   ├── UniGB-UCS2-H.bcmap
│   │           │   ├── UniGB-UCS2-V.bcmap
│   │           │   ├── UniGB-UTF16-H.bcmap
│   │           │   ├── UniGB-UTF16-V.bcmap
│   │           │   ├── UniGB-UTF32-H.bcmap
│   │           │   ├── UniGB-UTF32-V.bcmap
│   │           │   ├── UniGB-UTF8-H.bcmap
│   │           │   ├── UniGB-UTF8-V.bcmap
│   │           │   ├── UniJIS-UCS2-H.bcmap
│   │           │   ├── UniJIS-UCS2-HW-H.bcmap
│   │           │   ├── UniJIS-UCS2-HW-V.bcmap
│   │           │   ├── UniJIS-UCS2-V.bcmap
│   │           │   ├── UniJIS-UTF16-H.bcmap
│   │           │   ├── UniJIS-UTF16-V.bcmap
│   │           │   ├── UniJIS-UTF32-H.bcmap
│   │           │   ├── UniJIS-UTF32-V.bcmap
│   │           │   ├── UniJIS-UTF8-H.bcmap
│   │           │   ├── UniJIS-UTF8-V.bcmap
│   │           │   ├── UniJIS2004-UTF16-H.bcmap
│   │           │   ├── UniJIS2004-UTF16-V.bcmap
│   │           │   ├── UniJIS2004-UTF32-H.bcmap
│   │           │   ├── UniJIS2004-UTF32-V.bcmap
│   │           │   ├── UniJIS2004-UTF8-H.bcmap
│   │           │   ├── UniJIS2004-UTF8-V.bcmap
│   │           │   ├── UniJISPro-UCS2-HW-V.bcmap
│   │           │   ├── UniJISPro-UCS2-V.bcmap
│   │           │   ├── UniJISPro-UTF8-V.bcmap
│   │           │   ├── UniJISX0213-UTF32-H.bcmap
│   │           │   ├── UniJISX0213-UTF32-V.bcmap
│   │           │   ├── UniJISX02132004-UTF32-H.bcmap
│   │           │   ├── UniJISX02132004-UTF32-V.bcmap
│   │           │   ├── UniKS-UCS2-H.bcmap
│   │           │   ├── UniKS-UCS2-V.bcmap
│   │           │   ├── UniKS-UTF16-H.bcmap
│   │           │   ├── UniKS-UTF16-V.bcmap
│   │           │   ├── UniKS-UTF32-H.bcmap
│   │           │   ├── UniKS-UTF32-V.bcmap
│   │           │   ├── UniKS-UTF8-H.bcmap
│   │           │   ├── UniKS-UTF8-V.bcmap
│   │           │   ├── V.bcmap
│   │           │   └── WP-Symbol.bcmap
│   │           ├── debugger.css
│   │           ├── debugger.mjs
│   │           ├── iccs/
│   │           │   ├── CGATS001Compat-v2-micro.icc
│   │           │   └── LICENSE
│   │           ├── locale/
│   │           │   ├── ach/
│   │           │   │   └── viewer.ftl
│   │           │   ├── af/
│   │           │   │   └── viewer.ftl
│   │           │   ├── an/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ar/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ast/
│   │           │   │   └── viewer.ftl
│   │           │   ├── az/
│   │           │   │   └── viewer.ftl
│   │           │   ├── be/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── br/
│   │           │   │   └── viewer.ftl
│   │           │   ├── brx/
│   │           │   │   └── viewer.ftl
│   │           │   ├── bs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ca/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cak/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ckb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── cy/
│   │           │   │   └── viewer.ftl
│   │           │   ├── da/
│   │           │   │   └── viewer.ftl
│   │           │   ├── de/
│   │           │   │   └── viewer.ftl
│   │           │   ├── dsb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── el/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-CA/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-GB/
│   │           │   │   └── viewer.ftl
│   │           │   ├── en-US/
│   │           │   │   └── viewer.ftl
│   │           │   ├── eo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-AR/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-CL/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-ES/
│   │           │   │   └── viewer.ftl
│   │           │   ├── es-MX/
│   │           │   │   └── viewer.ftl
│   │           │   ├── et/
│   │           │   │   └── viewer.ftl
│   │           │   ├── eu/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fa/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ff/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fi/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fur/
│   │           │   │   └── viewer.ftl
│   │           │   ├── fy-NL/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ga-IE/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gd/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── gu-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── he/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hi-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hsb/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hu/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hy-AM/
│   │           │   │   └── viewer.ftl
│   │           │   ├── hye/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ia/
│   │           │   │   └── viewer.ftl
│   │           │   ├── id/
│   │           │   │   └── viewer.ftl
│   │           │   ├── is/
│   │           │   │   └── viewer.ftl
│   │           │   ├── it/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ja/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ka/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kab/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── km/
│   │           │   │   └── viewer.ftl
│   │           │   ├── kn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ko/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lij/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── locale.json
│   │           │   ├── lt/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ltg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── lv/
│   │           │   │   └── viewer.ftl
│   │           │   ├── meh/
│   │           │   │   └── viewer.ftl
│   │           │   ├── mk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ml/
│   │           │   │   └── viewer.ftl
│   │           │   ├── mr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ms/
│   │           │   │   └── viewer.ftl
│   │           │   ├── my/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nb-NO/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ne-NP/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── nn-NO/
│   │           │   │   └── viewer.ftl
│   │           │   ├── oc/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pa-IN/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pt-BR/
│   │           │   │   └── viewer.ftl
│   │           │   ├── pt-PT/
│   │           │   │   └── viewer.ftl
│   │           │   ├── rm/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ro/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ru/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sat/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sc/
│   │           │   │   └── viewer.ftl
│   │           │   ├── scn/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sco/
│   │           │   │   └── viewer.ftl
│   │           │   ├── si/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── skr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── son/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sq/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── sv-SE/
│   │           │   │   └── viewer.ftl
│   │           │   ├── szl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ta/
│   │           │   │   └── viewer.ftl
│   │           │   ├── te/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tg/
│   │           │   │   └── viewer.ftl
│   │           │   ├── th/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tl/
│   │           │   │   └── viewer.ftl
│   │           │   ├── tr/
│   │           │   │   └── viewer.ftl
│   │           │   ├── trs/
│   │           │   │   └── viewer.ftl
│   │           │   ├── uk/
│   │           │   │   └── viewer.ftl
│   │           │   ├── ur/
│   │           │   │   └── viewer.ftl
│   │           │   ├── uz/
│   │           │   │   └── viewer.ftl
│   │           │   ├── vi/
│   │           │   │   └── viewer.ftl
│   │           │   ├── wo/
│   │           │   │   └── viewer.ftl
│   │           │   ├── xh/
│   │           │   │   └── viewer.ftl
│   │           │   ├── zh-CN/
│   │           │   │   └── viewer.ftl
│   │           │   └── zh-TW/
│   │           │       └── viewer.ftl
│   │           ├── pdf.mjs
│   │           ├── pdf.sandbox.mjs
│   │           ├── pdf.worker.mjs
│   │           ├── standard_fonts/
│   │           │   ├── FoxitDingbats.pfb
│   │           │   ├── FoxitFixed.pfb
│   │           │   ├── FoxitFixedBold.pfb
│   │           │   ├── FoxitFixedBoldItalic.pfb
│   │           │   ├── FoxitFixedItalic.pfb
│   │           │   ├── FoxitSerif.pfb
│   │           │   ├── FoxitSerifBold.pfb
│   │           │   ├── FoxitSerifBoldItalic.pfb
│   │           │   ├── FoxitSerifItalic.pfb
│   │           │   ├── FoxitSymbol.pfb
│   │           │   ├── LICENSE_FOXIT
│   │           │   └── LICENSE_LIBERATION
│   │           ├── viewer.css
│   │           ├── viewer.html
│   │           ├── viewer.mjs
│   │           └── wasm/
│   │               ├── LICENSE_OPENJPEG
│   │               ├── LICENSE_PDFJS_OPENJPEG
│   │               ├── LICENSE_PDFJS_QCMS
│   │               ├── LICENSE_QCMS
│   │               ├── openjpeg.wasm
│   │               ├── openjpeg_nowasm_fallback.js
│   │               └── qcms_bg.wasm
│   ├── templates/
│   │   ├── 404.html
│   │   ├── account/
│   │   │   ├── email.html
│   │   │   ├── email_confirm.html
│   │   │   ├── login.html
│   │   │   ├── logout.html
│   │   │   ├── password_change.html
│   │   │   ├── password_reset.html
│   │   │   ├── password_reset_done.html
│   │   │   ├── password_reset_from_key.html
│   │   │   ├── password_reset_from_key_done.html
│   │   │   ├── password_set.html
│   │   │   ├── signup.html
│   │   │   └── signup_closed.html
│   │   ├── base.html
│   │   ├── frontend/
│   │   │   └── tandoor.html
│   │   ├── index.html
│   │   ├── manifest.json
│   │   ├── markdown_info.html
│   │   ├── no_groups_info.html
│   │   ├── no_perm_info.html
│   │   ├── offline.html
│   │   ├── openid/
│   │   │   └── login.html
│   │   ├── pdf_viewer.html
│   │   ├── rest_framework/
│   │   │   └── api.html
│   │   ├── search_info.html
│   │   ├── setup.html
│   │   ├── socialaccount/
│   │   │   ├── authentication_error.html
│   │   │   ├── connections.html
│   │   │   ├── login.html
│   │   │   ├── signup.html
│   │   │   └── snippets/
│   │   │       └── provider_list.html
│   │   ├── space_overview.html
│   │   ├── system.html
│   │   ├── test.html
│   │   └── test2.html
│   ├── templatetags/
│   │   ├── __init__.py
│   │   ├── custom_tags.py
│   │   └── theming_tags.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── docs/
│   │   │   │   └── reports/
│   │   │   │       └── tests/
│   │   │   │           ├── assets/
│   │   │   │           │   └── style.css
│   │   │   │           ├── pytest.xml
│   │   │   │           └── tests.html
│   │   │   ├── test_api_access_token.py
│   │   │   ├── test_api_ai_provider.py
│   │   │   ├── test_api_ai_timeout.py
│   │   │   ├── test_api_connector_config.py
│   │   │   ├── test_api_cook_log.py
│   │   │   ├── test_api_food.py
│   │   │   ├── test_api_food_shopping.py
│   │   │   ├── test_api_import_log.py
│   │   │   ├── test_api_ingredient.py
│   │   │   ├── test_api_invitelinke.py
│   │   │   ├── test_api_keyword.py
│   │   │   ├── test_api_meal_plan.py
│   │   │   ├── test_api_meal_type.py
│   │   │   ├── test_api_property.py
│   │   │   ├── test_api_property_type.py
│   │   │   ├── test_api_recipe.py
│   │   │   ├── test_api_recipe_book.py
│   │   │   ├── test_api_recipe_book_entry.py
│   │   │   ├── test_api_related_recipe.py
│   │   │   ├── test_api_share_link.py
│   │   │   ├── test_api_shopping_list_entryv2.py
│   │   │   ├── test_api_shopping_list_recipe.py
│   │   │   ├── test_api_shopping_recipe.py
│   │   │   ├── test_api_space.py
│   │   │   ├── test_api_step.py
│   │   │   ├── test_api_storage.py
│   │   │   ├── test_api_supermarket.py
│   │   │   ├── test_api_sync.py
│   │   │   ├── test_api_sync_log.py
│   │   │   ├── test_api_unit.py
│   │   │   ├── test_api_unit_conversion.py
│   │   │   ├── test_api_user.py
│   │   │   ├── test_api_userpreference.py
│   │   │   ├── test_api_userspace.py
│   │   │   └── test_api_view_log.py
│   │   ├── conftest.py
│   │   ├── docs/
│   │   │   └── reports/
│   │   │       └── tests/
│   │   │           ├── assets/
│   │   │           │   └── style.css
│   │   │           ├── pytest.xml
│   │   │           └── tests.html
│   │   ├── factories/
│   │   │   └── __init__.py
│   │   ├── other/
│   │   │   ├── __init__.py
│   │   │   ├── _recipes.py
│   │   │   ├── docs/
│   │   │   │   └── reports/
│   │   │   │       └── tests/
│   │   │   │           └── assets/
│   │   │   │               └── style.css
│   │   │   ├── test_automations.py
│   │   │   ├── test_connector_manager.py
│   │   │   ├── test_cooklang_integration.py
│   │   │   ├── test_data/
│   │   │   │   ├── Cooklang/
│   │   │   │   │   ├── American Pancakes.cook
│   │   │   │   │   ├── Another Lemon Blueberry Bread.cook
│   │   │   │   │   └── Butter Swirl Shortbread Cookies.cook
│   │   │   │   ├── allrecipes.html
│   │   │   │   ├── americastestkitchen.html
│   │   │   │   ├── chefkoch.html
│   │   │   │   ├── chefkoch2.html
│   │   │   │   ├── cookpad.html
│   │   │   │   ├── cookscountry.html
│   │   │   │   ├── delish.html
│   │   │   │   ├── foodnetwork.html
│   │   │   │   ├── giallozafferano.html
│   │   │   │   ├── journaldesfemmes.html
│   │   │   │   ├── madamedessert.html
│   │   │   │   ├── madamedessert.json
│   │   │   │   ├── marmiton.html
│   │   │   │   ├── regex_recipe.html
│   │   │   │   ├── tasteofhome.html
│   │   │   │   ├── thespruceeats.html
│   │   │   │   └── tudogostoso.html
│   │   │   ├── test_food_property.py
│   │   │   ├── test_ingredient_editor_performance.py
│   │   │   ├── test_ingredient_parser.py
│   │   │   ├── test_local_provider.py
│   │   │   ├── test_makenow_filter.py
│   │   │   ├── test_nested_serializer.py
│   │   │   ├── test_permission_helper.py
│   │   │   ├── test_recipe_full_text_search.py
│   │   │   ├── test_schemas.py
│   │   │   ├── test_social_auth.py
│   │   │   ├── test_theming.py
│   │   │   ├── test_unit_conversion.py
│   │   │   └── test_url_import.py
│   │   ├── resources/
│   │   │   └── websites/
│   │   │       ├── ld_json_1.html
│   │   │       ├── ld_json_2.html
│   │   │       ├── ld_json_3.html
│   │   │       ├── ld_json_4.html
│   │   │       ├── ld_json_itemList.html
│   │   │       ├── ld_json_multiple.html
│   │   │       ├── micro_data_1.html
│   │   │       ├── micro_data_2.html
│   │   │       ├── micro_data_3.html
│   │   │       └── micro_data_4.html
│   │   └── views/
│   │       ├── __init__.py
│   │       ├── test_views_api.py
│   │       └── test_views_general.py
│   ├── urls.py
│   ├── version_info.py
│   └── views/
│       ├── __init__.py
│       ├── api.py
│       ├── import_export.py
│       ├── telegram.py
│       └── views.py
├── docs/
│   ├── CNAME
│   ├── contribute/
│   │   ├── contribute.md
│   │   ├── documentation.md
│   │   ├── feature_contrib/
│   │   │   ├── Integration.md
│   │   │   └── featureguides.md
│   │   ├── guidelines.md
│   │   ├── installation.md
│   │   ├── pycharm.md
│   │   ├── related.md
│   │   ├── translations.md
│   │   └── vscode.md
│   ├── faq.md
│   ├── features/
│   │   ├── ai.md
│   │   ├── authentication.md
│   │   ├── automation.md
│   │   ├── connectors.md
│   │   ├── external_recipes.md
│   │   ├── import_export.md
│   │   ├── shopping.md
│   │   ├── telegram_bot.md
│   │   └── templating.md
│   ├── index.md
│   ├── install/
│   │   ├── archlinux.md
│   │   ├── docker/
│   │   │   ├── apache-proxy/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── ipv6_plain/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── nginx-proxy/
│   │   │   │   └── docker-compose.yml
│   │   │   ├── plain/
│   │   │   │   └── docker-compose.yml
│   │   │   └── traefik-nginx/
│   │   │       └── docker-compose.yml
│   │   ├── docker.md
│   │   ├── helmChart.md
│   │   ├── homeassistant.md
│   │   ├── k8s/
│   │   │   ├── 10-configmap.yaml
│   │   │   ├── 15-secrets.yaml
│   │   │   ├── 20-service-account.yaml
│   │   │   ├── 30-pvc.yaml
│   │   │   ├── 40-sts-postgresql.yaml
│   │   │   ├── 45-service-db.yaml
│   │   │   ├── 50-deployment.yaml
│   │   │   ├── 60-service.yaml
│   │   │   └── 70-ingress.yaml
│   │   ├── kubernetes.md
│   │   ├── kubesail.md
│   │   ├── manual.md
│   │   ├── other.md
│   │   ├── swag.md
│   │   ├── synology.md
│   │   ├── truenas_portainer.md
│   │   ├── unraid.md
│   │   └── wsl.md
│   ├── preview.xcf
│   ├── stylesheets/
│   │   └── extra.css
│   ├── system/
│   │   ├── backup.md
│   │   ├── configuration.md
│   │   ├── migration_sqlite-postgres.md
│   │   ├── permissions.md
│   │   └── updating.md
│   └── tests/
│       ├── assets/
│       │   └── style.css
│       ├── pytest.xml
│       └── tests.html
├── generate_api_client.py
├── http.d/
│   ├── Recipes.conf.template
│   └── errorpages/
│       └── http502.html
├── make_compile_messages.py
├── manage.py
├── mkdocs.yml
├── nginx/
│   └── conf.d/
│       ├── Recipes.conf
│       └── errorpages/
│           └── http502.html
├── openapitools.json
├── plugin.py
├── pyproject.toml
├── pytest.ini
├── recipes/
│   ├── __init__.py
│   ├── locale/
│   │   ├── ar/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── bg/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ca/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── da/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── el/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── en/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── es/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── he/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hu_HU/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── hy/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── id/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── lv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nb_NO/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── rn/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ro/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── sv/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── tr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── uk/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── vi/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   ├── zh_CN/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── django.mo
│   │   │       └── django.po
│   │   └── zh_Hant/
│   │       └── LC_MESSAGES/
│   │           ├── django.mo
│   │           └── django.po
│   ├── middleware.py
│   ├── settings.py
│   ├── test_settings.py
│   ├── urls.py
│   └── wsgi.py
├── requirements.txt
├── version.py
└── vue3/
    ├── env.d.ts
    ├── package.json
    ├── src/
    │   ├── apps/
    │   │   └── tandoor/
    │   │       ├── Tandoor.vue
    │   │       └── main.ts
    │   ├── assets/
    │   │   ├── bookmarklet_v3.js
    │   │   └── vueform.css
    │   ├── components/
    │   │   ├── buttons/
    │   │   │   ├── AiActionButton.vue
    │   │   │   └── BtnCopy.vue
    │   │   ├── dialogs/
    │   │   │   ├── AddToShoppingDialog.vue
    │   │   │   ├── AutoPlanDialog.vue
    │   │   │   ├── BatchDeleteDialog.vue
    │   │   │   ├── BatchEditFoodDialog.vue
    │   │   │   ├── BatchEditRecipeDialog.vue
    │   │   │   ├── DeleteConfirmDialog.vue
    │   │   │   ├── FdcSearchDialog.vue
    │   │   │   ├── FreezerExpiryDialog.vue
    │   │   │   ├── HelpDialog.vue
    │   │   │   ├── ImportTandoorDialog.vue
    │   │   │   ├── InventoryEntryLogDialog.vue
    │   │   │   ├── MealPlanIcalDialog.vue
    │   │   │   ├── MessageListDialog.vue
    │   │   │   ├── ModelEditDialog.vue
    │   │   │   ├── ModelMergeDialog.vue
    │   │   │   ├── RecipeScalingDialog.vue
    │   │   │   ├── RecipeShareDialog.vue
    │   │   │   ├── ShoppingExportDialog.vue
    │   │   │   ├── ShoppingLineItemDialog.vue
    │   │   │   ├── StepIngredientSorterDialog.vue
    │   │   │   ├── SyncDialog.vue
    │   │   │   └── VClosableCardTitle.vue
    │   │   ├── display/
    │   │   │   ├── BookEntryCard.vue
    │   │   │   ├── ClosableHelpAlert.vue
    │   │   │   ├── DatabaseLinkCol.vue
    │   │   │   ├── DatabaseModelCol.vue
    │   │   │   ├── ExternalRecipeViewer.vue
    │   │   │   ├── HelpView.vue
    │   │   │   ├── HorizontalMealPlanWindow.vue
    │   │   │   ├── HorizontalRecipeWindow.vue
    │   │   │   ├── ImportLogViewer.vue
    │   │   │   ├── IngredientString.vue
    │   │   │   ├── IngredientsTable.vue
    │   │   │   ├── IngredientsTableRow.vue
    │   │   │   ├── Instructions.vue
    │   │   │   ├── InventoryEntryTable.vue
    │   │   │   ├── KeywordsBar.vue
    │   │   │   ├── MealPlanCalendarHeader.vue
    │   │   │   ├── MealPlanCalendarItem.vue
    │   │   │   ├── MealPlanView.vue
    │   │   │   ├── NavigationDrawerContextMenu.vue
    │   │   │   ├── PrivateRecipeBadge.vue
    │   │   │   ├── PropertyView.vue
    │   │   │   ├── RandomIcon.vue
    │   │   │   ├── RecipeActivity.vue
    │   │   │   ├── RecipeCard.vue
    │   │   │   ├── RecipeImage.vue
    │   │   │   ├── RecipeView.vue
    │   │   │   ├── ScalableNumber.vue
    │   │   │   ├── ShoppingLineItem.vue
    │   │   │   ├── ShoppingListView.vue
    │   │   │   ├── ShoppingListsBar.vue
    │   │   │   ├── SpaceLimitsInfo.vue
    │   │   │   ├── StepView.vue
    │   │   │   ├── StepsOverview.vue
    │   │   │   ├── ThankYouNote.vue
    │   │   │   ├── Timer.vue
    │   │   │   └── VSnackbarQueued.vue
    │   │   ├── inputs/
    │   │   │   ├── BaseUnitSelect.vue
    │   │   │   ├── CategorySelectChip.vue
    │   │   │   ├── GlobalSearchDialog.vue
    │   │   │   ├── HierarchyEditor.vue
    │   │   │   ├── LanguageSelect.vue
    │   │   │   ├── ModelSelect.vue
    │   │   │   ├── ModelSelectVuetify.vue
    │   │   │   ├── NumberScalerDialog.vue
    │   │   │   ├── PropertiesEditor.vue
    │   │   │   ├── RatingField.vue
    │   │   │   ├── RecipeContextMenu.vue
    │   │   │   ├── ShoppingListEntryInput.vue
    │   │   │   ├── ShoppingListSelectChip.vue
    │   │   │   ├── StepEditor.vue
    │   │   │   ├── StepMarkdownEditor.vue
    │   │   │   └── UserFileField.vue
    │   │   ├── model_editors/
    │   │   │   ├── AccessTokenEditor.vue
    │   │   │   ├── AiProviderEditor.vue
    │   │   │   ├── AutomationEditor.vue
    │   │   │   ├── ConnectorConfigEditor.vue
    │   │   │   ├── CookLogEditor.vue
    │   │   │   ├── CustomFilterEditor.vue
    │   │   │   ├── FoodEditor.vue
    │   │   │   ├── HouseholdEditor.vue
    │   │   │   ├── InventoryLocationEditor.vue
    │   │   │   ├── InviteLinkEditor.vue
    │   │   │   ├── KeywordEditor.vue
    │   │   │   ├── MealPlanEditor.vue
    │   │   │   ├── MealTypeEditor.vue
    │   │   │   ├── ModelEditorBase.vue
    │   │   │   ├── PropertyEditor.vue
    │   │   │   ├── PropertyTypeEditor.vue
    │   │   │   ├── RecipeBookEditor.vue
    │   │   │   ├── RecipeEditor.vue
    │   │   │   ├── ShoppingListEditor.vue
    │   │   │   ├── ShoppingListEntryEditor.vue
    │   │   │   ├── SpaceEditor.vue
    │   │   │   ├── StorageEditor.vue
    │   │   │   ├── SupermarketCategoryEditor.vue
    │   │   │   ├── SupermarketEditor.vue
    │   │   │   ├── SyncEditor.vue
    │   │   │   ├── UnitConversionEditor.vue
    │   │   │   ├── UnitEditor.vue
    │   │   │   ├── UserFileEditor.vue
    │   │   │   └── UserSpaceEditor.vue
    │   │   ├── settings/
    │   │   │   ├── AccountSettings.vue
    │   │   │   ├── ApiSettings.vue
    │   │   │   ├── CosmeticSettings.vue
    │   │   │   ├── ExportDataSettings.vue
    │   │   │   ├── MealPlanDeviceSettings.vue
    │   │   │   ├── MealPlanSettings.vue
    │   │   │   ├── OpenDataImportSettings.vue
    │   │   │   ├── SearchSettings.vue
    │   │   │   ├── ShoppingSettings.vue
    │   │   │   └── SpaceSettings.vue
    │   │   └── tables/
    │   │       └── InventoryEntryLogTable.vue
    │   ├── composables/
    │   │   ├── useDjangoUrls.ts
    │   │   ├── useFileApi.ts
    │   │   ├── useModelEditorFunctions.ts
    │   │   └── useNavigation.ts
    │   ├── i18n.ts
    │   ├── locales/
    │   │   ├── ar.json
    │   │   ├── bg.json
    │   │   ├── ca.json
    │   │   ├── cs.json
    │   │   ├── da.json
    │   │   ├── de.json
    │   │   ├── el.json
    │   │   ├── en.json
    │   │   ├── es.json
    │   │   ├── fi.json
    │   │   ├── fr.json
    │   │   ├── he.json
    │   │   ├── hr.json
    │   │   ├── hu.json
    │   │   ├── hy.json
    │   │   ├── id.json
    │   │   ├── is.json
    │   │   ├── it.json
    │   │   ├── ko.json
    │   │   ├── lt.json
    │   │   ├── lv.json
    │   │   ├── nb_NO.json
    │   │   ├── nl.json
    │   │   ├── nn.json
    │   │   ├── pl.json
    │   │   ├── pt.json
    │   │   ├── pt_BR.json
    │   │   ├── ro.json
    │   │   ├── ru.json
    │   │   ├── sl.json
    │   │   ├── sv.json
    │   │   ├── tr.json
    │   │   ├── uk.json
    │   │   ├── zh_Hans.json
    │   │   └── zh_Hant.json
    │   ├── openapi/
    │   │   ├── .openapi-generator/
    │   │   │   ├── FILES
    │   │   │   └── VERSION
    │   │   ├── .openapi-generator-ignore
    │   │   ├── apis/
    │   │   │   ├── ApiApi.ts
    │   │   │   ├── ApiTokenAuthApi.ts
    │   │   │   └── index.ts
    │   │   ├── index.ts
    │   │   ├── models/
    │   │   │   ├── AccessToken.ts
    │   │   │   ├── AiLog.ts
    │   │   │   ├── AiProvider.ts
    │   │   │   ├── AlignmentEnum.ts
    │   │   │   ├── AuthToken.ts
    │   │   │   ├── AutoMealPlan.ts
    │   │   │   ├── Automation.ts
    │   │   │   ├── AutomationTypeEnum.ts
    │   │   │   ├── BaseUnitEnum.ts
    │   │   │   ├── BookingTypeEnum.ts
    │   │   │   ├── BookmarkletImport.ts
    │   │   │   ├── BookmarkletImportList.ts
    │   │   │   ├── ConnectorConfig.ts
    │   │   │   ├── ConnectorConfigConfig.ts
    │   │   │   ├── ConnectorConfigTypeEnum.ts
    │   │   │   ├── CookLog.ts
    │   │   │   ├── CustomFilter.ts
    │   │   │   ├── DefaultPageEnum.ts
    │   │   │   ├── DeleteEnum.ts
    │   │   │   ├── EnterpriseKeyword.ts
    │   │   │   ├── EnterpriseSocialEmbed.ts
    │   │   │   ├── EnterpriseSocialEmbedTypeEnum.ts
    │   │   │   ├── EnterpriseSocialRecipeSearch.ts
    │   │   │   ├── EnterpriseSpace.ts
    │   │   │   ├── ExportLog.ts
    │   │   │   ├── ExportRequest.ts
    │   │   │   ├── FdcQuery.ts
    │   │   │   ├── FdcQueryFoods.ts
    │   │   │   ├── Food.ts
    │   │   │   ├── FoodBatchUpdate.ts
    │   │   │   ├── FoodInheritField.ts
    │   │   │   ├── FoodShopping.ts
    │   │   │   ├── FoodShoppingUpdate.ts
    │   │   │   ├── FoodSimple.ts
    │   │   │   ├── GenericModel.ts
    │   │   │   ├── GenericModelReference.ts
    │   │   │   ├── Group.ts
    │   │   │   ├── Household.ts
    │   │   │   ├── ImportImage.ts
    │   │   │   ├── ImportLog.ts
    │   │   │   ├── ImportOpenData.ts
    │   │   │   ├── ImportOpenDataMetaData.ts
    │   │   │   ├── ImportOpenDataResponse.ts
    │   │   │   ├── ImportOpenDataResponseDetail.ts
    │   │   │   ├── ImportOpenDataVersionMetaData.ts
    │   │   │   ├── Ingredient.ts
    │   │   │   ├── IngredientParserRequest.ts
    │   │   │   ├── IngredientParserResponse.ts
    │   │   │   ├── IngredientSimple.ts
    │   │   │   ├── IngredientString.ts
    │   │   │   ├── InventoryEntry.ts
    │   │   │   ├── InventoryLocation.ts
    │   │   │   ├── InventoryLog.ts
    │   │   │   ├── InviteLink.ts
    │   │   │   ├── Keyword.ts
    │   │   │   ├── KeywordLabel.ts
    │   │   │   ├── Localization.ts
    │   │   │   ├── MealPlan.ts
    │   │   │   ├── MealType.ts
    │   │   │   ├── MethodEnum.ts
    │   │   │   ├── NutritionInformation.ts
    │   │   │   ├── OpenDataCategory.ts
    │   │   │   ├── OpenDataConversion.ts
    │   │   │   ├── OpenDataFood.ts
    │   │   │   ├── OpenDataFoodProperty.ts
    │   │   │   ├── OpenDataProperty.ts
    │   │   │   ├── OpenDataStore.ts
    │   │   │   ├── OpenDataStoreCategory.ts
    │   │   │   ├── OpenDataUnit.ts
    │   │   │   ├── OpenDataUnitTypeEnum.ts
    │   │   │   ├── OpenDataVersion.ts
    │   │   │   ├── PaginatedAiLogList.ts
    │   │   │   ├── PaginatedAiProviderList.ts
    │   │   │   ├── PaginatedAutomationList.ts
    │   │   │   ├── PaginatedBookmarkletImportListList.ts
    │   │   │   ├── PaginatedConnectorConfigConfigList.ts
    │   │   │   ├── PaginatedConnectorConfigList.ts
    │   │   │   ├── PaginatedCookLogList.ts
    │   │   │   ├── PaginatedCustomFilterList.ts
    │   │   │   ├── PaginatedEnterpriseSocialEmbedList.ts
    │   │   │   ├── PaginatedEnterpriseSocialRecipeSearchList.ts
    │   │   │   ├── PaginatedEnterpriseSpaceList.ts
    │   │   │   ├── PaginatedExportLogList.ts
    │   │   │   ├── PaginatedFoodList.ts
    │   │   │   ├── PaginatedGenericModelList.ts
    │   │   │   ├── PaginatedGenericModelReferenceList.ts
    │   │   │   ├── PaginatedHouseholdList.ts
    │   │   │   ├── PaginatedImportLogList.ts
    │   │   │   ├── PaginatedIngredientList.ts
    │   │   │   ├── PaginatedInventoryEntryList.ts
    │   │   │   ├── PaginatedInventoryLocationList.ts
    │   │   │   ├── PaginatedInventoryLogList.ts
    │   │   │   ├── PaginatedInviteLinkList.ts
    │   │   │   ├── PaginatedKeywordList.ts
    │   │   │   ├── PaginatedMealPlanList.ts
    │   │   │   ├── PaginatedMealTypeList.ts
    │   │   │   ├── PaginatedOpenDataCategoryList.ts
    │   │   │   ├── PaginatedOpenDataConversionList.ts
    │   │   │   ├── PaginatedOpenDataFoodList.ts
    │   │   │   ├── PaginatedOpenDataPropertyList.ts
    │   │   │   ├── PaginatedOpenDataStoreList.ts
    │   │   │   ├── PaginatedOpenDataUnitList.ts
    │   │   │   ├── PaginatedOpenDataVersionList.ts
    │   │   │   ├── PaginatedPropertyList.ts
    │   │   │   ├── PaginatedPropertyTypeList.ts
    │   │   │   ├── PaginatedRecipeBookEntryList.ts
    │   │   │   ├── PaginatedRecipeBookList.ts
    │   │   │   ├── PaginatedRecipeImportList.ts
    │   │   │   ├── PaginatedRecipeOverviewList.ts
    │   │   │   ├── PaginatedRecipeSimpleList.ts
    │   │   │   ├── PaginatedShoppingListEntryList.ts
    │   │   │   ├── PaginatedShoppingListList.ts
    │   │   │   ├── PaginatedShoppingListRecipeList.ts
    │   │   │   ├── PaginatedSpaceList.ts
    │   │   │   ├── PaginatedStepList.ts
    │   │   │   ├── PaginatedStorageEntryList.ts
    │   │   │   ├── PaginatedStorageList.ts
    │   │   │   ├── PaginatedStorageLocationList.ts
    │   │   │   ├── PaginatedSupermarketCategoryList.ts
    │   │   │   ├── PaginatedSupermarketCategoryRelationList.ts
    │   │   │   ├── PaginatedSupermarketList.ts
    │   │   │   ├── PaginatedSyncList.ts
    │   │   │   ├── PaginatedSyncLogList.ts
    │   │   │   ├── PaginatedUnitConversionList.ts
    │   │   │   ├── PaginatedUnitList.ts
    │   │   │   ├── PaginatedUserFileList.ts
    │   │   │   ├── PaginatedUserSpaceList.ts
    │   │   │   ├── PaginatedViewLogList.ts
    │   │   │   ├── ParsedIngredient.ts
    │   │   │   ├── PatchedAccessToken.ts
    │   │   │   ├── PatchedAiProvider.ts
    │   │   │   ├── PatchedAutomation.ts
    │   │   │   ├── PatchedBookmarkletImport.ts
    │   │   │   ├── PatchedConnectorConfig.ts
    │   │   │   ├── PatchedConnectorConfigConfig.ts
    │   │   │   ├── PatchedCookLog.ts
    │   │   │   ├── PatchedCustomFilter.ts
    │   │   │   ├── PatchedEnterpriseSocialEmbed.ts
    │   │   │   ├── PatchedEnterpriseSpace.ts
    │   │   │   ├── PatchedExportLog.ts
    │   │   │   ├── PatchedFood.ts
    │   │   │   ├── PatchedHousehold.ts
    │   │   │   ├── PatchedImportLog.ts
    │   │   │   ├── PatchedIngredient.ts
    │   │   │   ├── PatchedInventoryEntry.ts
    │   │   │   ├── PatchedInventoryLocation.ts
    │   │   │   ├── PatchedInviteLink.ts
    │   │   │   ├── PatchedKeyword.ts
    │   │   │   ├── PatchedMealPlan.ts
    │   │   │   ├── PatchedMealType.ts
    │   │   │   ├── PatchedOpenDataCategory.ts
    │   │   │   ├── PatchedOpenDataConversion.ts
    │   │   │   ├── PatchedOpenDataFood.ts
    │   │   │   ├── PatchedOpenDataProperty.ts
    │   │   │   ├── PatchedOpenDataStore.ts
    │   │   │   ├── PatchedOpenDataUnit.ts
    │   │   │   ├── PatchedOpenDataVersion.ts
    │   │   │   ├── PatchedProperty.ts
    │   │   │   ├── PatchedPropertyType.ts
    │   │   │   ├── PatchedRecipe.ts
    │   │   │   ├── PatchedRecipeBook.ts
    │   │   │   ├── PatchedRecipeBookEntry.ts
    │   │   │   ├── PatchedRecipeImport.ts
    │   │   │   ├── PatchedSearchPreference.ts
    │   │   │   ├── PatchedShoppingList.ts
    │   │   │   ├── PatchedShoppingListEntry.ts
    │   │   │   ├── PatchedShoppingListRecipe.ts
    │   │   │   ├── PatchedSpace.ts
    │   │   │   ├── PatchedStep.ts
    │   │   │   ├── PatchedStorage.ts
    │   │   │   ├── PatchedStorageEntry.ts
    │   │   │   ├── PatchedStorageLocation.ts
    │   │   │   ├── PatchedSupermarket.ts
    │   │   │   ├── PatchedSupermarketCategory.ts
    │   │   │   ├── PatchedSupermarketCategoryRelation.ts
    │   │   │   ├── PatchedSync.ts
    │   │   │   ├── PatchedUnit.ts
    │   │   │   ├── PatchedUnitConversion.ts
    │   │   │   ├── PatchedUser.ts
    │   │   │   ├── PatchedUserPreference.ts
    │   │   │   ├── PatchedUserSpace.ts
    │   │   │   ├── PatchedViewLog.ts
    │   │   │   ├── Property.ts
    │   │   │   ├── PropertyType.ts
    │   │   │   ├── Recipe.ts
    │   │   │   ├── RecipeBatchUpdate.ts
    │   │   │   ├── RecipeBook.ts
    │   │   │   ├── RecipeBookEntry.ts
    │   │   │   ├── RecipeFlat.ts
    │   │   │   ├── RecipeFromSource.ts
    │   │   │   ├── RecipeFromSourceResponse.ts
    │   │   │   ├── RecipeImage.ts
    │   │   │   ├── RecipeImport.ts
    │   │   │   ├── RecipeOverview.ts
    │   │   │   ├── RecipeShoppingUpdate.ts
    │   │   │   ├── RecipeSimple.ts
    │   │   │   ├── ScalingEnum.ts
    │   │   │   ├── SearchEnum.ts
    │   │   │   ├── SearchFields.ts
    │   │   │   ├── SearchPreference.ts
    │   │   │   ├── ServerSettings.ts
    │   │   │   ├── ShareLink.ts
    │   │   │   ├── ShoppingList.ts
    │   │   │   ├── ShoppingListEntry.ts
    │   │   │   ├── ShoppingListEntryBulk.ts
    │   │   │   ├── ShoppingListEntryBulkCreate.ts
    │   │   │   ├── ShoppingListEntrySimpleCreate.ts
    │   │   │   ├── ShoppingListRecipe.ts
    │   │   │   ├── SourceImportDuplicate.ts
    │   │   │   ├── SourceImportFood.ts
    │   │   │   ├── SourceImportIngredient.ts
    │   │   │   ├── SourceImportKeyword.ts
    │   │   │   ├── SourceImportProperty.ts
    │   │   │   ├── SourceImportPropertyType.ts
    │   │   │   ├── SourceImportRecipe.ts
    │   │   │   ├── SourceImportStep.ts
    │   │   │   ├── SourceImportUnit.ts
    │   │   │   ├── Space.ts
    │   │   │   ├── SpaceNavTextColorEnum.ts
    │   │   │   ├── SpaceThemeEnum.ts
    │   │   │   ├── Step.ts
    │   │   │   ├── Storage.ts
    │   │   │   ├── Supermarket.ts
    │   │   │   ├── SupermarketCategory.ts
    │   │   │   ├── SupermarketCategoryRelation.ts
    │   │   │   ├── Sync.ts
    │   │   │   ├── SyncLog.ts
    │   │   │   ├── ThemeEnum.ts
    │   │   │   ├── TypeEnum.ts
    │   │   │   ├── Unit.ts
    │   │   │   ├── UnitConversion.ts
    │   │   │   ├── User.ts
    │   │   │   ├── UserFile.ts
    │   │   │   ├── UserFileView.ts
    │   │   │   ├── UserPreference.ts
    │   │   │   ├── UserPreferenceNavTextColorEnum.ts
    │   │   │   ├── UserSpace.ts
    │   │   │   ├── ViewLog.ts
    │   │   │   └── index.ts
    │   │   ├── openapi.json
    │   │   ├── openapitools.json
    │   │   ├── runtime.ts
    │   │   └── templates/
    │   │       ├── apis.mustache
    │   │       └── modelGeneric.mustache
    │   ├── pages/
    │   │   ├── 404Page.vue
    │   │   ├── BookViewPage.vue
    │   │   ├── BooksPage.vue
    │   │   ├── DatabasePage.vue
    │   │   ├── HelpPage.vue
    │   │   ├── IngredientEditorPage.vue
    │   │   ├── InventoryBookingPage.vue
    │   │   ├── MealPlanPage.vue
    │   │   ├── ModelDeletePage.vue
    │   │   ├── ModelEditPage.vue
    │   │   ├── ModelListPage.vue
    │   │   ├── PantryPage.vue
    │   │   ├── PropertyEditorPage.vue
    │   │   ├── RecipeImportPage.vue
    │   │   ├── RecipeViewPage.vue
    │   │   ├── SearchPage.vue
    │   │   ├── SettingsPage.vue
    │   │   ├── ShoppingListPage.vue
    │   │   ├── SpaceSetupPage.vue
    │   │   ├── StartPage.vue
    │   │   ├── TestPage.vue
    │   │   └── WelcomePage.vue
    │   ├── service-worker.ts
    │   ├── stores/
    │   │   ├── MealPlanStore.ts
    │   │   ├── MessageStore.ts
    │   │   ├── ShoppingStore.ts
    │   │   └── UserPreferenceStore.ts
    │   ├── types/
    │   │   ├── FoodFilters.ts
    │   │   ├── MealPlan.ts
    │   │   ├── Models.ts
    │   │   ├── Plugins.ts
    │   │   ├── SearchTypes.ts
    │   │   ├── Shopping.ts
    │   │   └── settings.ts
    │   ├── utils/
    │   │   ├── breakpoint_utils.ts
    │   │   ├── cookie.ts
    │   │   ├── date_utils.ts
    │   │   ├── fdc.ts
    │   │   ├── integration_utils.ts
    │   │   ├── logic_utils.ts
    │   │   ├── model_utils.ts
    │   │   ├── number_utils.ts
    │   │   ├── step_utils.ts
    │   │   └── utils.ts
    │   └── vuetify.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    ├── tsconfig.vitest.json
    └── vite.config.ts
Download .txt
Showing preview only (906K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (10776 symbols across 653 files)

FILE: cookbook/admin.py
  class CustomUserAdmin (line 25) | class CustomUserAdmin(UserAdmin):
    method has_add_permission (line 26) | def has_add_permission(self, request, obj=None):
  function delete_space_action (line 37) | def delete_space_action(modeladmin, request, queryset):
  class SpaceAdmin (line 42) | class SpaceAdmin(admin.ModelAdmin):
  class UserSpaceAdmin (line 55) | class UserSpaceAdmin(admin.ModelAdmin):
  class UserPreferenceAdmin (line 65) | class UserPreferenceAdmin(admin.ModelAdmin):
    method name (line 73) | def name(obj):
  class SearchPreferenceAdmin (line 80) | class SearchPreferenceAdmin(admin.ModelAdmin):
    method name (line 86) | def name(obj):
  class AiProviderAdmin (line 93) | class AiProviderAdmin(admin.ModelAdmin):
  class AiLogAdmin (line 101) | class AiLogAdmin(admin.ModelAdmin):
  class StorageAdmin (line 107) | class StorageAdmin(admin.ModelAdmin):
  class ConnectorConfigAdmin (line 115) | class ConnectorConfigAdmin(admin.ModelAdmin):
  class CustomFilterAdmin (line 123) | class CustomFilterAdmin(admin.ModelAdmin):
  class SyncAdmin (line 130) | class SyncAdmin(admin.ModelAdmin):
  class SupermarketCategoryInline (line 138) | class SupermarketCategoryInline(admin.TabularInline):
  class SupermarketAdmin (line 142) | class SupermarketAdmin(admin.ModelAdmin):
  class SupermarketCategoryAdmin (line 147) | class SupermarketCategoryAdmin(admin.ModelAdmin):
  class SyncLogAdmin (line 155) | class SyncLogAdmin(admin.ModelAdmin):
  function enable_tree_sorting (line 163) | def enable_tree_sorting(modeladmin, request, queryset):
  function disable_tree_sorting (line 172) | def disable_tree_sorting(modeladmin, request, queryset):
  function sort_tree (line 178) | def sort_tree(modeladmin, request, queryset):
  class KeywordAdmin (line 186) | class KeywordAdmin(TreeAdmin):
  function delete_unattached_steps (line 197) | def delete_unattached_steps(modeladmin, request, queryset):
  class StepAdmin (line 202) | class StepAdmin(admin.ModelAdmin):
    method recipe_and_name (line 210) | def recipe_and_name(obj):
  function rebuild_index (line 220) | def rebuild_index(modeladmin, request, queryset):
  class RecipeAdmin (line 230) | class RecipeAdmin(admin.ModelAdmin):
    method created_by (line 238) | def created_by(obj):
  class UnitAdmin (line 248) | class UnitAdmin(admin.ModelAdmin):
  class FoodAdmin (line 260) | class FoodAdmin(TreeAdmin):
  class UnitConversionAdmin (line 270) | class UnitConversionAdmin(admin.ModelAdmin):
  function delete_unattached_ingredients (line 279) | def delete_unattached_ingredients(modeladmin, request, queryset):
  class IngredientAdmin (line 284) | class IngredientAdmin(admin.ModelAdmin):
    method recipe_name (line 291) | def recipe_name(obj):
  class CommentAdmin (line 299) | class CommentAdmin(admin.ModelAdmin):
    method name (line 305) | def name(obj):
  class RecipeImportAdmin (line 312) | class RecipeImportAdmin(admin.ModelAdmin):
  class RecipeBookAdmin (line 319) | class RecipeBookAdmin(admin.ModelAdmin):
    method user_name (line 324) | def user_name(obj):
  class RecipeBookEntryAdmin (line 331) | class RecipeBookEntryAdmin(admin.ModelAdmin):
  class MealPlanAdmin (line 338) | class MealPlanAdmin(admin.ModelAdmin):
    method user (line 342) | def user(obj):
  class MealTypeAdmin (line 349) | class MealTypeAdmin(admin.ModelAdmin):
  class ViewLogAdmin (line 357) | class ViewLogAdmin(admin.ModelAdmin):
  class InviteLinkAdmin (line 364) | class InviteLinkAdmin(admin.ModelAdmin):
  class CookLogAdmin (line 374) | class CookLogAdmin(admin.ModelAdmin):
  class ShoppingListRecipeAdmin (line 382) | class ShoppingListRecipeAdmin(admin.ModelAdmin):
  class ShoppingListEntryAdmin (line 389) | class ShoppingListEntryAdmin(admin.ModelAdmin):
  class ShareLinkAdmin (line 396) | class ShareLinkAdmin(admin.ModelAdmin):
  function delete_properties_with_type (line 404) | def delete_properties_with_type(modeladmin, request, queryset):
  class PropertyTypeAdmin (line 409) | class PropertyTypeAdmin(admin.ModelAdmin):
  class PropertyAdmin (line 419) | class PropertyAdmin(admin.ModelAdmin):
  class NutritionInformationAdmin (line 426) | class NutritionInformationAdmin(admin.ModelAdmin):
  class ImportLogAdmin (line 433) | class ImportLogAdmin(admin.ModelAdmin):
  class TelegramBotAdmin (line 440) | class TelegramBotAdmin(admin.ModelAdmin):
  class BookmarkletImportAdmin (line 447) | class BookmarkletImportAdmin(admin.ModelAdmin):
  class UserFileAdmin (line 454) | class UserFileAdmin(admin.ModelAdmin):

FILE: cookbook/apps.py
  class CookbookConfig (line 12) | class CookbookConfig(AppConfig):
    method ready (line 15) | def ready(self):

FILE: cookbook/connectors/connector.py
  class UserDTO (line 9) | class UserDTO:
    method create_from_user (line 14) | def create_from_user(instance: User) -> 'UserDTO':
  class ShoppingListEntryDTO (line 22) | class ShoppingListEntryDTO:
    method try_create_from_entry (line 30) | def try_create_from_entry(instance: ShoppingListEntry) -> Optional['Sh...
  class Connector (line 44) | class Connector(ABC):
    method __init__ (line 46) | def __init__(self, config: ConnectorConfig):
    method on_shopping_list_entry_created (line 50) | async def on_shopping_list_entry_created(self, instance: ShoppingListE...
    method on_shopping_list_entry_updated (line 55) | async def on_shopping_list_entry_updated(self, instance: ShoppingListE...
    method on_shopping_list_entry_deleted (line 59) | async def on_shopping_list_entry_deleted(self, instance: ShoppingListE...
    method close (line 63) | async def close(self) -> None:

FILE: cookbook/connectors/connector_manager.py
  class ActionType (line 22) | class ActionType(Enum):
  class Work (line 29) | class Work:
  class Singleton (line 34) | class Singleton(type):
    method __call__ (line 37) | def __call__(cls, *args, **kwargs):
  class ConnectorManager (line 52) | class ConnectorManager(metaclass=Singleton):
    method __init__ (line 57) | def __init__(self):
    method __call__ (line 65) | def __call__(self, instance: Any, **kwargs) -> None:
    method _add_work (line 78) | def _add_work(self, action_type: ActionType, *instances: REGISTERED_CL...
    method stop (line 88) | def stop(self):
    method is_initialized (line 93) | def is_initialized(cls):
    method add_work (line 97) | def add_work(action_type: ActionType, *instances: REGISTERED_CLASSES):
    method worker (line 108) | def worker(worker_id: int, worker_queue: queue.Queue):
    method get_connected_for_config (line 176) | def get_connected_for_config(config: ConnectorConfig) -> Optional[Conn...
  function _force_load_instance (line 184) | def _force_load_instance(instance: REGISTERED_CLASSES):
  function _close_connectors (line 191) | async def _close_connectors(connectors: List[Connector]):
  function run_connectors (line 203) | async def run_connectors(connectors: List[Connector], instance: REGISTER...

FILE: cookbook/connectors/homeassistant.py
  class HomeAssistant (line 12) | class HomeAssistant(Connector):
    method __init__ (line 16) | def __init__(self, config: ConnectorConfig):
    method homeassistant_api_call (line 26) | async def homeassistant_api_call(self, method: str, path: str, data: D...
    method on_shopping_list_entry_created (line 35) | async def on_shopping_list_entry_created(self, shopping_list_entry: Sh...
    method on_shopping_list_entry_updated (line 56) | async def on_shopping_list_entry_updated(self, shopping_list_entry: Sh...
    method on_shopping_list_entry_deleted (line 61) | async def on_shopping_list_entry_deleted(self, shopping_list_entry: Sh...
    method close (line 80) | async def close(self) -> None:
  function _format_shopping_list_entry (line 84) | def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntryDT...

FILE: cookbook/forms.py
  class SelectWidget (line 18) | class SelectWidget(widgets.Select):
    class Media (line 20) | class Media:
  class MultiSelectWidget (line 24) | class MultiSelectWidget(widgets.SelectMultiple):
    class Media (line 26) | class Media:
  class ImportExportBase (line 30) | class ImportExportBase(forms.Form):
  class MultipleFileInput (line 67) | class MultipleFileInput(forms.ClearableFileInput):
  class MultipleFileField (line 71) | class MultipleFileField(forms.FileField):
    method __init__ (line 73) | def __init__(self, *args, **kwargs):
    method clean (line 77) | def clean(self, data, initial=None):
  class ImportForm (line 86) | class ImportForm(ImportExportBase):
  class ExportForm (line 96) | class ExportForm(ImportExportBase):
    method __init__ (line 101) | def __init__(self, *args, **kwargs):
  class InviteLinkForm (line 107) | class InviteLinkForm(forms.ModelForm):
    method __init__ (line 109) | def __init__(self, *args, **kwargs):
    method clean (line 114) | def clean(self):
    method clean_email (line 121) | def clean_email(self):
    class Meta (line 129) | class Meta:
  class SpaceCreateForm (line 140) | class SpaceCreateForm(forms.Form):
    method clean_name (line 144) | def clean_name(self):
  class SpaceJoinForm (line 152) | class SpaceJoinForm(forms.Form):
  class AllAuthSignupForm (line 157) | class AllAuthSignupForm(SignupForm):
    method __init__ (line 161) | def __init__(self, **kwargs):
    method signup (line 168) | def signup(self, request, user):
  class AllAuthSocialSignupForm (line 172) | class AllAuthSocialSignupForm(SocialSignupForm):
    method __init__ (line 175) | def __init__(self, **kwargs):
    method signup (line 180) | def signup(self, request, user):
  class CustomPasswordResetForm (line 193) | class CustomPasswordResetForm(ResetPasswordForm):
    method __init__ (line 196) | def __init__(self, **kwargs):
  class UserCreateForm (line 202) | class UserCreateForm(forms.Form):

FILE: cookbook/helper/AllAuthCustomAdapter.py
  class AllAuthCustomAdapter (line 18) | class AllAuthCustomAdapter(DefaultAccountAdapter):
    method is_open_for_signup (line 20) | def is_open_for_signup(self, request):
    method send_mail (line 37) | def send_mail(self, template_prefix, email, context):

FILE: cookbook/helper/CustomStorageClass.py
  class CachedS3Boto3Storage (line 8) | class CachedS3Boto3Storage(S3Boto3Storage):
    method url (line 9) | def url(self, name, **kwargs):

FILE: cookbook/helper/HelperFunctions.py
  class Round (line 15) | class Round(Func):
  function str2bool (line 20) | def str2bool(v):
  function safe_request (line 27) | def safe_request(method, url, **kwargs):
  function match_or_fuzzymatch (line 43) | def match_or_fuzzymatch(check_string: str, key_dict: dict) -> tuple[str,...

FILE: cookbook/helper/ai_helper.py
  function get_monthly_token_usage (line 11) | def get_monthly_token_usage(space):
  function has_monthly_token (line 21) | def has_monthly_token(space):
  function can_perform_ai_request (line 28) | def can_perform_ai_request(space):
  class AiCallbackHandler (line 32) | class AiCallbackHandler(CustomLogger):
    method __init__ (line 38) | def __init__(self, space, user, ai_provider, function):
    method log_pre_api_call (line 45) | def log_pre_api_call(self, model, messages, kwargs):
    method log_post_api_call (line 48) | def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
    method log_success_event (line 51) | def log_success_event(self, kwargs, response_obj, start_time, end_time):
    method log_failure_event (line 54) | def log_failure_event(self, kwargs, response_obj, start_time, end_time):
    method create_ai_log (line 57) | def create_ai_log(self, kwargs, response_obj, start_time, end_time):

FILE: cookbook/helper/automation_helper.py
  class AutomationEngine (line 9) | class AutomationEngine:
    method __init__ (line 26) | def __init__(self, request, use_cache=True, source=None):
    method apply_keyword_automation (line 34) | def apply_keyword_automation(self, keyword):
    method apply_unit_automation (line 58) | def apply_unit_automation(self, unit):
    method apply_food_automation (line 82) | def apply_food_automation(self, food):
    method apply_never_unit_automation (line 107) | def apply_never_unit_automation(self, tokens):
    method apply_transpose_automation (line 149) | def apply_transpose_automation(self, string):
    method apply_regex_replace_automation (line 185) | def apply_regex_replace_automation(self, string, automation_type):

FILE: cookbook/helper/batch_edit_helper.py
  function add_to_relation (line 1) | def add_to_relation(relation_model, base_field_name, base_ids, related_f...
  function remove_from_relation (line 12) | def remove_from_relation(relation_model, base_field_name, base_ids, rela...
  function remove_all_from_relation (line 16) | def remove_all_from_relation(relation_model, base_field_name, base_ids):
  function set_relation (line 20) | def set_relation(relation_model, base_field_name, base_ids, related_fiel...

FILE: cookbook/helper/cache_helper.py
  class CacheHelper (line 1) | class CacheHelper:
    method __init__ (line 7) | def __init__(self, space):

FILE: cookbook/helper/context_processors.py
  function context_settings (line 4) | def context_settings(request):

FILE: cookbook/helper/cooklang_parser.py
  class Quantity (line 11) | class Quantity:
    method parse (line 16) | def parse(cls, raw) -> "Quantity":
    method add_optional (line 40) | def add_optional(cls, a: Optional["Quantity"], b: Optional["Quantity"]...
    method __add__ (line 47) | def __add__(self, other: "Quantity") -> "Quantity":
  class Timer (line 58) | class Timer:
    method parse (line 64) | def parse(cls, raw) -> "Timer":
  class Ingredient (line 88) | class Ingredient:
    method parse (line 93) | def parse(cls, raw: str) -> "Ingredient":
    method __add__ (line 104) | def __add__(self, other: "Ingredient") -> "Ingredient":
  class Block (line 114) | class Block:
    method new (line 119) | def new(cls):
  class Step (line 124) | class Step:
    method parse (line 128) | def parse(cls, raw: str) -> "Step":
  class Recipe (line 211) | class Recipe:
    method parse (line 217) | def parse(cls, raw: str) -> "Recipe":

FILE: cookbook/helper/drf_spectacular_hooks.py
  function custom_postprocessing_hook (line 6) | def custom_postprocessing_hook(result, generator, request, public):
  class LegacySchema (line 25) | class LegacySchema(AutoSchema):
    method path (line 29) | def path(self):
    method get_operation_id (line 34) | def get_operation_id(self):
    method get_operation_id_base (line 50) | def get_operation_id_base(self, action):
    method get_serializer (line 87) | def get_serializer(self):
    method _to_camel_case (line 98) | def _to_camel_case(self, snake_str):

FILE: cookbook/helper/fdc_helper.py
  function get_all_nutrient_types (line 4) | def get_all_nutrient_types():

FILE: cookbook/helper/image_processing.py
  function rescale_image_jpeg (line 7) | def rescale_image_jpeg(image_object, base_width=1020):
  function rescale_image_png (line 20) | def rescale_image_png(image_object, base_width=1020):
  function get_filetype (line 31) | def get_filetype(name):
  function is_file_type_allowed (line 38) | def is_file_type_allowed(filename, image_only=False):
  function strip_image_meta (line 53) | def strip_image_meta(image_object, file_format):
  function handle_image (line 69) | def handle_image(request, image_object, filetype):

FILE: cookbook/helper/ingredient_parser.py
  class IngredientParser (line 10) | class IngredientParser:
    method __init__ (line 15) | def __init__(self, request, cache_mode=True, ignore_automations=False):
    method get_unit (line 27) | def get_unit(self, unit_name):
    method get_food (line 45) | def get_food(self, food_name):
    method parse_fraction (line 63) | def parse_fraction(self, x):
    method parse_amount (line 77) | def parse_amount(self, x):
    method parse_food_with_comma (line 121) | def parse_food_with_comma(self, tokens):
    method parse_food (line 136) | def parse_food(self, tokens):
    method parse (line 161) | def parse(self, ingredient):
    method parse_as_ingredient (line 298) | def parse_as_ingredient(self, text):

FILE: cookbook/helper/mdx_attributes.py
  class StyleTreeprocessor (line 5) | class StyleTreeprocessor(Treeprocessor):
    method run_processor (line 7) | def run_processor(self, node):
    method run (line 18) | def run(self, root):
  class MarkdownFormatExtension (line 23) | class MarkdownFormatExtension(markdown.Extension):
    method extendMarkdown (line 25) | def extendMarkdown(self, md):

FILE: cookbook/helper/mdx_urlize.py
  class UrlizePattern (line 51) | class UrlizePattern(markdown.inlinepatterns.Pattern):
    method handleMatch (line 54) | def handleMatch(self, m):
  class UrlizeExtension (line 74) | class UrlizeExtension(markdown.Extension):
    method extendMarkdown (line 77) | def extendMarkdown(self, md):
  function makeExtension (line 82) | def makeExtension(*args, **kwargs):

FILE: cookbook/helper/open_data_importer.py
  class OpenDataImportResponse (line 12) | class OpenDataImportResponse:
    method to_dict (line 18) | def to_dict(self):
  class OpenDataImporter (line 22) | class OpenDataImporter:
    method __init__ (line 29) | def __init__(self, request, data, update_existing=False, use_metric=Tr...
    method _update_slug_cache (line 35) | def _update_slug_cache(self, object_class, datatype):
    method _is_obj_identical (line 39) | def _is_obj_identical(field_list, obj, existing_obj):
    method _merge_if_conflicting (line 62) | def _merge_if_conflicting(model_type, obj, existing_data_slugs, existi...
    method _get_existing_obj (line 89) | def _get_existing_obj(obj, existing_data_slugs, existing_data_names):
    method import_units (line 109) | def import_units(self):
    method import_category (line 157) | def import_category(self):
    method import_property (line 204) | def import_property(self):
    method import_supermarket (line 253) | def import_supermarket(self):
    method import_food (line 318) | def import_food(self):
    method import_conversion (line 448) | def import_conversion(self):

FILE: cookbook/helper/permission_config.py
  class PermissionConfig (line 4) | class PermissionConfig:

FILE: cookbook/helper/permission_helper.py
  function get_allowed_groups (line 22) | def get_allowed_groups(groups_required):
  function has_group_permission (line 37) | def has_group_permission(user, groups, no_cache=False):
  function is_object_owner (line 69) | def is_object_owner(user, obj):
  function is_space_owner (line 86) | def is_space_owner(user, obj):
  function is_object_shared (line 101) | def is_object_shared(user, obj):
  function is_object_household (line 116) | def is_object_household(user, obj):
  function share_link_valid (line 130) | def share_link_valid(recipe, share):
  function group_required (line 156) | def group_required(*groups_required):
  class GroupRequiredMixin (line 170) | class GroupRequiredMixin(object):
    method dispatch (line 177) | def dispatch(self, request, *args, **kwargs):
  class OwnerRequiredMixin (line 196) | class OwnerRequiredMixin(object):
    method dispatch (line 198) | def dispatch(self, request, *args, **kwargs):
  class CustomIsOwner (line 223) | class CustomIsOwner(permissions.BasePermission):
    method has_permission (line 231) | def has_permission(self, request, view):
    method has_object_permission (line 234) | def has_object_permission(self, request, view, obj):
  class CustomIsOwnerReadOnly (line 238) | class CustomIsOwnerReadOnly(CustomIsOwner):
    method has_permission (line 239) | def has_permission(self, request, view):
    method has_object_permission (line 242) | def has_object_permission(self, request, view, obj):
  class CustomIsOwnerDestroyOnly (line 246) | class CustomIsOwnerDestroyOnly(CustomIsOwner):
    method has_permission (line 247) | def has_permission(self, request, view):
    method has_object_permission (line 250) | def has_object_permission(self, request, view, obj):
  class CustomIsSpaceOwner (line 254) | class CustomIsSpaceOwner(permissions.BasePermission):
    method has_permission (line 261) | def has_permission(self, request, view):
    method has_object_permission (line 264) | def has_object_permission(self, request, view, obj):
  class CustomIsShared (line 269) | class CustomIsShared(permissions.BasePermission):
    method has_permission (line 276) | def has_permission(self, request, view):
    method has_object_permission (line 279) | def has_object_permission(self, request, view, obj):
  class CustomIsHousehold (line 283) | class CustomIsHousehold(permissions.BasePermission):
    method has_permission (line 290) | def has_permission(self, request, view):
    method has_object_permission (line 293) | def has_object_permission(self, request, view, obj):
  class CustomIsGuest (line 297) | class CustomIsGuest(permissions.BasePermission):
    method has_permission (line 304) | def has_permission(self, request, view):
    method has_object_permission (line 307) | def has_object_permission(self, request, view, obj):
  class CustomIsUser (line 311) | class CustomIsUser(permissions.BasePermission):
    method has_permission (line 318) | def has_permission(self, request, view):
  class CustomIsAdmin (line 322) | class CustomIsAdmin(permissions.BasePermission):
    method has_permission (line 329) | def has_permission(self, request, view):
  class CustomIsShare (line 333) | class CustomIsShare(permissions.BasePermission):
    method has_permission (line 340) | def has_permission(self, request, view):
    method has_object_permission (line 343) | def has_object_permission(self, request, view, obj):
  class CustomRecipePermission (line 350) | class CustomRecipePermission(permissions.BasePermission):
    method has_permission (line 356) | def has_permission(self, request, view):  # user is either at least a ...
    method has_object_permission (line 361) | def has_object_permission(self, request, view, obj):
  class CustomAiProviderPermission (line 378) | class CustomAiProviderPermission(permissions.BasePermission):
    method has_permission (line 387) | def has_permission(self, request, view):  # user is either at least a ...
    method has_object_permission (line 391) | def has_object_permission(self, request, view, obj):
  class CustomUserPermission (line 397) | class CustomUserPermission(permissions.BasePermission):
    method has_permission (line 403) | def has_permission(self, request, view):  # a space filtered user list...
    method has_object_permission (line 406) | def has_object_permission(self, request, view, obj):  # object write p...
  class CustomTokenHasScope (line 415) | class CustomTokenHasScope(TokenHasScope):
    method has_permission (line 422) | def has_permission(self, request, view):
  class CustomTokenHasReadWriteScope (line 429) | class CustomTokenHasReadWriteScope(TokenHasReadWriteScope):
    method get_scopes (line 436) | def get_scopes(self, request, view):
    method has_permission (line 444) | def has_permission(self, request, view):
  function above_space_limit (line 451) | def above_space_limit(space):  # TODO add file storage limit
  function above_space_recipe_limit (line 462) | def above_space_recipe_limit(space):
  function above_space_user_limit (line 474) | def above_space_user_limit(space):
  function switch_user_active_space (line 486) | def switch_user_active_space(user, space):
  class IsReadOnlyDRF (line 506) | class IsReadOnlyDRF(permissions.BasePermission):
    method has_permission (line 509) | def has_permission(self, request, view):
  class IsCreateDRF (line 513) | class IsCreateDRF(permissions.BasePermission):
    method has_permission (line 516) | def has_permission(self, request, view):
  function create_space_for_user (line 520) | def create_space_for_user(user, name=None):

FILE: cookbook/helper/property_helper.py
  class FoodPropertyHelper (line 8) | class FoodPropertyHelper:
    method __init__ (line 11) | def __init__(self, space):
    method calculate_recipe_properties (line 18) | def calculate_recipe_properties(self, recipe):
    method add_or_create (line 86) | def add_or_create(d, key, value, food):

FILE: cookbook/helper/recipe_search.py
  class RecipeSearch (line 17) | class RecipeSearch():
    method __init__ (line 20) | def __init__(self, request, **params):
    method get_queryset (line 143) | def get_queryset(self, queryset):
    method _sort_includes (line 169) | def _sort_includes(self, *args):
    method _build_sort_order (line 177) | def _build_sort_order(self):
    method string_filters (line 211) | def string_filters(self, string=None):
    method _cooked_on_filter (line 247) | def _cooked_on_filter(self):
    method _viewed_on_filter (line 263) | def _viewed_on_filter(self, viewed_date=None):
    method _created_on_filter (line 275) | def _created_on_filter(self):
    method _updated_on_filter (line 283) | def _updated_on_filter(self):
    method _created_by_filter (line 291) | def _created_by_filter(self, created_by_user_id=None):
    method _new_recipes (line 296) | def _new_recipes(self, new_days=7):
    method _recently_viewed (line 305) | def _recently_viewed(self, num_recent=None):
    method _favorite_recipes (line 318) | def _favorite_recipes(self):
    method keyword_filters (line 338) | def keyword_filters(self, **kwargs):
    method food_filters (line 371) | def food_filters(self, **kwargs):
    method unit_filters (line 405) | def unit_filters(self, units=None, operator=True):
    method rating_filter (line 414) | def rating_filter(self):
    method internal_filter (line 427) | def internal_filter(self, internal=None):
    method book_filters (line 432) | def book_filters(self, **kwargs):
    method step_filters (line 457) | def step_filters(self, steps=None, operator=True):
    method build_fulltext_filters (line 466) | def build_fulltext_filters(self, string=None):
    method build_text_filters (line 497) | def build_text_filters(self, string=None):
    method build_trigram (line 510) | def build_trigram(self, string=None):
    method _makenow_filter (line 526) | def _makenow_filter(self, missing=None):
    method __children_substitute_filter (line 550) | def __children_substitute_filter(shopping_users=None):
    method __sibling_substitute_filter (line 563) | def __sibling_substitute_filter(shopping_users=None):

FILE: cookbook/helper/recipe_url_import.py
  function get_from_scraper (line 17) | def get_from_scraper(scrape, request):
  function get_recipe_properties (line 216) | def get_recipe_properties(space, property_data):
  function get_from_youtube_scraper (line 247) | def get_from_youtube_scraper(url, request):
  function parse_name (line 284) | def parse_name(name):
  function parse_description (line 293) | def parse_description(description):
  function clean_instruction_string (line 297) | def clean_instruction_string(instruction):
  function parse_instructions (line 336) | def parse_instructions(instructions):
  function parse_image (line 365) | def parse_image(image):
  function parse_servings (line 385) | def parse_servings(servings):
  function parse_servings_text (line 399) | def parse_servings_text(servings):
  function parse_time (line 413) | def parse_time(recipe_time):
  function parse_keywords (line 433) | def parse_keywords(keyword_json, request):
  function listify_keywords (line 452) | def listify_keywords(keyword_list):
  function normalize_string (line 468) | def normalize_string(string):
  function iso_duration_to_minutes (line 479) | def iso_duration_to_minutes(string):
  function get_images_from_soup (line 484) | def get_images_from_soup(soup, url):
  function clean_dict (line 517) | def clean_dict(input_dict, key):

FILE: cookbook/helper/scope_middleware.py
  class ScopeMiddleware (line 15) | class ScopeMiddleware:
    method __init__ (line 16) | def __init__(self, get_response):
    method __call__ (line 19) | def __call__(self, request):

FILE: cookbook/helper/shopping_helper.py
  function shopping_helper (line 13) | def shopping_helper(qs, request):
  class RecipeShoppingEditor (line 35) | class RecipeShoppingEditor():
    method __init__ (line 36) | def __init__(self, user, space, **kwargs):
    method _recipe_servings (line 65) | def _recipe_servings(self):
    method _servings_factor (line 70) | def _servings_factor(self):
    method get_shopping_list_recipe (line 75) | def get_shopping_list_recipe(id, user, space):
    method get_recipe_ingredients (line 87) | def get_recipe_ingredients(self, id, exclude_onhand=False):
    method _include_related (line 99) | def _include_related(self):
    method _exclude_onhand (line 103) | def _exclude_onhand(self):
    method create (line 106) | def create(self, **kwargs):
    method add (line 139) | def add(self, **kwargs):
    method edit (line 142) | def edit(self, servings=None, ingredients=None, **kwargs):
    method edit_servings (line 153) | def edit_servings(self, servings=None, **kwargs):
    method delete (line 172) | def delete(self, **kwargs):
    method _add_ingredients (line 179) | def _add_ingredients(self, ingredients=None):
    method _delete_ingredients (line 207) | def _delete_ingredients(self, ingredients=None):

FILE: cookbook/helper/template_helper.py
  class IngredientObject (line 14) | class IngredientObject(object):
    method __init__ (line 21) | def __init__(self, ingredient):
    method __str__ (line 49) | def __str__(self):
  function render_instructions (line 56) | def render_instructions(step):  # TODO deduplicate markdown cleanup code

FILE: cookbook/helper/unit_conversion_helper.py
  class ConversionException (line 37) | class ConversionException(Exception):
  class UnitConversionHelper (line 41) | class UnitConversionHelper:
    method __init__ (line 45) | def __init__(self, space):
    method convert_from_to (line 53) | def convert_from_to(from_unit, to_unit, amount):
    method base_conversions (line 72) | def base_conversions(self, ingredient_list):
    method get_conversions (line 110) | def get_conversions(self, ingredient):
    method _uc_convert (line 144) | def _uc_convert(self, uc, amount, unit, food):

FILE: cookbook/integration/cheftap.py
  class ChefTap (line 8) | class ChefTap(Integration):
    method import_file_name_filter (line 10) | def import_file_name_filter(self, zip_info_object):
    method get_recipe_from_file (line 14) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 58) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/chowdown.py
  class Chowdown (line 13) | class Chowdown(Integration):
    method import_file_name_filter (line 15) | def import_file_name_filter(self, zip_info_object):
    method normalize_name (line 18) | def normalize_name(self, name):
    method get_recipe_from_file (line 25) | def get_recipe_from_file(self, file):
    method formatTime (line 169) | def formatTime(self, min):
    method get_file_from_recipe (line 174) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 244) | def get_files_from_recipes(self, recipes, el, cookie):

FILE: cookbook/integration/cookbookapp.py
  class CookBookApp (line 18) | class CookBookApp(Integration):
    method import_file_name_filter (line 20) | def import_file_name_filter(self, zip_info_object):
    method get_recipe_from_file (line 23) | def get_recipe_from_file(self, file):

FILE: cookbook/integration/cooklang.py
  class Cooklang (line 15) | class Cooklang(Integration):
    method apply_metadata_cooklang_to_tandoor (line 17) | def apply_metadata_cooklang_to_tandoor(self, cooklang_metadata: dict, ...
    method get_recipe_from_file (line 56) | def get_recipe_from_file(self, file) -> Recipe:

FILE: cookbook/integration/cookmate.py
  class Cookmate (line 10) | class Cookmate(Integration):
    method import_file_name_filter (line 12) | def import_file_name_filter(self, zip_info_object):
    method get_files_from_recipes (line 15) | def get_files_from_recipes(self, recipes, el, cookie):
    method get_recipe_from_file (line 18) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 79) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/copymethat.py
  class CopyMeThat (line 14) | class CopyMeThat(Integration):
    method import_file_name_filter (line 16) | def import_file_name_filter(self, zip_info_object):
    method get_recipe_from_file (line 21) | def get_recipe_from_file(self, file):
    method split_recipe_file (line 128) | def split_recipe_file(self, file):

FILE: cookbook/integration/default.py
  class Default (line 14) | class Default(Integration):
    method get_recipe_from_file (line 16) | def get_recipe_from_file(self, file):
    method decode_recipe (line 29) | def decode_recipe(self, string):
    method get_file_from_recipe (line 59) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 65) | def get_files_from_recipes(self, recipes, el, cookie):

FILE: cookbook/integration/domestica.py
  class Domestica (line 10) | class Domestica(Integration):
    method get_recipe_from_file (line 12) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 53) | def get_file_from_recipe(self, recipe):
    method split_recipe_file (line 56) | def split_recipe_file(self, file):

FILE: cookbook/integration/gourmet.py
  class Gourmet (line 16) | class Gourmet(Integration):
    method split_recipe_file (line 18) | def split_recipe_file(self, file):
    method get_ingredients_recursive (line 25) | def get_ingredients_recursive(self, step, ingredients, ingredient_pars...
    method get_recipe_from_file (line 61) | def get_recipe_from_file(self, file):
    method get_files_from_recipes (line 206) | def get_files_from_recipes(self, recipes, el, cookie):
    method get_file_from_recipe (line 209) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/integration.py
  class Integration (line 23) | class Integration:
    method __init__ (line 36) | def __init__(self, request, export_type):
    method do_export (line 59) | def do_export(self, recipes, el):
    method import_file_name_filter (line 93) | def import_file_name_filter(self, zip_info_object):
    method do_import (line 103) | def do_import(self, files, il, import_duplicates, meal_plans=True, sho...
    method handle_duplicates (line 246) | def handle_duplicates(self, recipe, import_duplicates):
    method import_recipe_image (line 256) | def import_recipe_image(self, recipe, image_file, filetype='.jpeg'):
    method get_recipe_from_file (line 266) | def get_recipe_from_file(self, file):
    method split_recipe_file (line 274) | def split_recipe_file(self, file):
    method get_file_from_recipe (line 282) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 293) | def get_files_from_recipes(self, recipes, el, cookie):
    method handle_exception (line 304) | def handle_exception(exception, log=None, message=''):
    method get_export_file_name (line 313) | def get_export_file_name(self, format='zip'):
    method get_recipe_processed_msg (line 316) | def get_recipe_processed_msg(self, recipe):

FILE: cookbook/integration/mealie.py
  class Mealie (line 13) | class Mealie(Integration):
    method import_file_name_filter (line 15) | def import_file_name_filter(self, zip_info_object):
    method get_recipe_from_file (line 18) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 98) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/mealie1.py
  class Mealie1 (line 20) | class Mealie1(Integration):
    method get_recipe_from_file (line 25) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 370) | def get_file_from_recipe(self, recipe):
  function get_step_id (line 374) | def get_step_id(i, first_step_of_recipe_dict, step_id_dict, recipe_ingre...

FILE: cookbook/integration/mealmaster.py
  class MealMaster (line 8) | class MealMaster(Integration):
    method get_recipe_from_file (line 10) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 58) | def get_file_from_recipe(self, recipe):
    method split_recipe_file (line 61) | def split_recipe_file(self, file):

FILE: cookbook/integration/melarecipes.py
  class MelaRecipes (line 12) | class MelaRecipes(Integration):
    method split_recipe_file (line 14) | def split_recipe_file(self, file):
    method get_files_from_recipes (line 17) | def get_files_from_recipes(self, recipes, el, cookie):
    method get_recipe_from_file (line 20) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 82) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/nextcloud_cookbook.py
  class NextcloudCookbook (line 15) | class NextcloudCookbook(Integration):
    method import_file_name_filter (line 17) | def import_file_name_filter(self, zip_info_object):
    method get_recipe_from_file (line 20) | def get_recipe_from_file(self, file):
    method formatTime (line 106) | def formatTime(self, min):
    method get_file_from_recipe (line 111) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 143) | def get_files_from_recipes(self, recipes, el, cookie):
    method getJPEG (line 172) | def getJPEG(self, imageByte):
    method getThumb (line 180) | def getThumb(self, size, imageByte):

FILE: cookbook/integration/openeats.py
  class OpenEats (line 10) | class OpenEats(Integration):
    method get_recipe_from_file (line 12) | def get_recipe_from_file(self, file):
    method split_recipe_file (line 69) | def split_recipe_file(self, file):
    method get_file_from_recipe (line 128) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/paprika.py
  class Paprika (line 15) | class Paprika(Integration):
    method get_file_from_recipe (line 17) | def get_file_from_recipe(self, recipe):
    method get_recipe_from_file (line 20) | def get_recipe_from_file(self, file):

FILE: cookbook/integration/pdfexport.py
  class PDFexport (line 10) | class PDFexport(Integration):
    method get_recipe_from_file (line 12) | def get_recipe_from_file(self, file):
    method get_files_from_recipes_async (line 15) | async def get_files_from_recipes_async(self, recipes, el, cookie):
    method get_files_from_recipes (line 55) | def get_files_from_recipes(self, recipes, el, cookie):

FILE: cookbook/integration/pepperplate.py
  class Pepperplate (line 6) | class Pepperplate(Integration):
    method get_recipe_from_file (line 8) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 54) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/plantoeat.py
  class Plantoeat (line 10) | class Plantoeat(Integration):
    method get_recipe_from_file (line 12) | def get_recipe_from_file(self, file):
    method split_recipe_file (line 83) | def split_recipe_file(self, file):

FILE: cookbook/integration/recettetek.py
  class RecetteTek (line 15) | class RecetteTek(Integration):
    method import_file_name_filter (line 17) | def import_file_name_filter(self, zip_info_object):
    method split_recipe_file (line 21) | def split_recipe_file(self, file):
    method get_recipe_from_file (line 29) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 137) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/recipekeeper.py
  class RecipeKeeper (line 14) | class RecipeKeeper(Integration):
    method import_file_name_filter (line 16) | def import_file_name_filter(self, zip_info_object):
    method split_recipe_file (line 19) | def split_recipe_file(self, file):
    method get_recipe_from_file (line 23) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 86) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/recipesage.py
  class RecipeSage (line 12) | class RecipeSage(Integration):
    method get_recipe_from_file (line 14) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 91) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 120) | def get_files_from_recipes(self, recipes, el, cookie):
    method split_recipe_file (line 131) | def split_recipe_file(self, file):

FILE: cookbook/integration/rezeptsuitede.py
  class Rezeptsuitede (line 11) | class Rezeptsuitede(Integration):
    method split_recipe_file (line 13) | def split_recipe_file(self, file):
    method get_recipe_from_file (line 16) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 77) | def get_file_from_recipe(self, recipe):

FILE: cookbook/integration/rezkonv.py
  class RezKonv (line 6) | class RezKonv(Integration):
    method get_recipe_from_file (line 8) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 57) | def get_file_from_recipe(self, recipe):
    method split_recipe_file (line 60) | def split_recipe_file(self, file):

FILE: cookbook/integration/saffron.py
  class Saffron (line 8) | class Saffron(Integration):
    method get_recipe_from_file (line 10) | def get_recipe_from_file(self, file):
    method get_file_from_recipe (line 60) | def get_file_from_recipe(self, recipe):
    method get_files_from_recipes (line 89) | def get_files_from_recipes(self, recipes, el, cookie):

FILE: cookbook/management/commands/export.py
  class Command (line 5) | class Command(DumpdataCommand):
    method handle (line 6) | def handle(self, *args, **options):

FILE: cookbook/management/commands/fix_duplicate_properties.py
  class Command (line 14) | class Command(BaseCommand):
    method add_arguments (line 17) | def add_arguments(self, parser):
    method handle (line 20) | def handle(self, *args, **options):

FILE: cookbook/management/commands/import.py
  class Command (line 5) | class Command(LoaddataCommand):
    method handle (line 6) | def handle(self, *args, **options):

FILE: cookbook/management/commands/rebuildindex.py
  class Command (line 13) | class Command(BaseCommand):
    method handle (line 16) | def handle(self, *args, **options):

FILE: cookbook/management/commands/seed_basic_data.py
  class Command (line 13) | class Command(BaseCommand):
    method handle (line 16) | def handle(self, *args, **options):

FILE: cookbook/managers.py
  class RecipeSearchManager (line 24) | class RecipeSearchManager(models.Manager):
    method search (line 25) | def search(self, search_text, space):

FILE: cookbook/migrations/0001_initial.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0001_squashed_0227_space_ai_default_provider_and_more.py
  function allSearchFields (line 21) | def allSearchFields():
  function nameSearchField (line 25) | def nameSearchField():
  function create_default_groups (line 29) | def create_default_groups(apps, schema_editor):
  function create_fields (line 39) | def create_fields(apps, schema_editor):
  class Migration (line 59) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0002_auto_20191119_2035.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0003_enable_pgtrm.py
  class Migration (line 5) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0004_storage_created_by.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0005_recipebook_recipebookentry.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0006_recipe_image.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0007_auto_20191226_0852.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0008_mealplan.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0009_auto_20200130_1056.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0010_auto_20200130_1059.py
  function migrate_ingredient_units (line 7) | def migrate_ingredient_units(apps, schema_editor):
  class Migration (line 22) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0011_remove_recipeingredients_unit.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0012_auto_20200130_1116.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0013_userpreference.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0014_auto_20200213_2332.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0015_auto_20200213_2334.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0016_auto_20200213_2335.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0017_auto_20200216_2257.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0018_auto_20200216_2303.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0019_ingredient.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0020_recipeingredient_ingredient.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0021_auto_20200216_2309.py
  function migrate_ingredients (line 6) | def migrate_ingredients(apps, schema_editor):
  class Migration (line 21) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0022_remove_recipeingredient_name.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0023_auto_20200216_2311.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0024_auto_20200216_2313.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0025_userpreference_nav_color.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0026_auto_20200219_1605.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0027_ingredient_recipe.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0028_auto_20200317_1901.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0029_auto_20200317_1901.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0030_recipeingredient_note.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0031_auto_20200407_1841.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0032_userpreference_default_unit.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0033_userpreference_default_page.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0034_auto_20200426_1614.py
  function apply_migration (line 7) | def apply_migration(apps, schema_editor):
  class Migration (line 17) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0035_auto_20200427_1637.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0036_auto_20200427_1800.py
  function apply_migration (line 7) | def apply_migration(apps, schema_editor):
  class Migration (line 17) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0037_userpreference_search_style.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0038_auto_20200502_1259.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0039_recipebook_shared.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0040_auto_20200502_1433.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0041_auto_20200502_1446.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0042_cooklog.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0043_auto_20200507_2302.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0044_viewlog.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0045_userpreference_show_recent.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0046_auto_20200602_1133.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0047_auto_20200602_1133.py
  function migrate_meal_types (line 8) | def migrate_meal_types(apps, schema_editor):
  class Migration (line 46) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0048_auto_20200602_1140.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0049_mealtype_created_by.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0050_auto_20200611_1509.py
  function migrate_meal_types (line 8) | def migrate_meal_types(apps, schema_editor):
  class Migration (line 25) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0051_auto_20200611_1518.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0052_userpreference_ingredient_decimals.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0053_auto_20200611_2217.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0054_sharelink.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0055_auto_20200616_1236.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0056_auto_20200625_2118.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0056_auto_20200625_2157.py
  function invalidate_shares (line 9) | def invalidate_shares(apps, schema_editor):
  class Migration (line 16) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0057_auto_20200625_2127.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0058_auto_20200625_2128.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0059_auto_20200625_2137.py
  function migrate_ingredients (line 7) | def migrate_ingredients(apps, schema_editor):
  class Migration (line 18) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0060_auto_20200625_2144.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0061_merge_20200625_2209.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0062_auto_20200625_2219.py
  function create_default_step (line 7) | def create_default_step(apps, schema_editor):
  class Migration (line 23) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0063_auto_20200625_2230.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0064_auto_20200625_2329.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0065_auto_20200626_1444.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0066_auto_20200626_1455.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0067_auto_20200629_1508.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0068_auto_20200629_2127.py
  function convert_old_specials (line 8) | def convert_old_specials(apps, schema_editor):
  class Migration (line 30) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0069_auto_20200629_2134.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0070_auto_20200701_2007.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0071_auto_20200701_2048.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0072_step_show_as_header.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0073_auto_20200708_2311.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0074_remove_keyword_created_by.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0075_shoppinglist_shoppinglistentry_shoppinglistrecipe.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0076_shoppinglist_entries.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0077_invitelink.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0078_invitelink_used_by.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0079_invitelink_group.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0080_auto_20200921_2331.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0081_auto_20200921_2349.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0082_auto_20200922_1143.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0083_space.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0084_auto_20200922_1233.py
  function create_default_space (line 6) | def create_default_space(apps, schema_editor):
  class Migration (line 15) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0085_auto_20200922_1235.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0086_auto_20200929_1143.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0087_auto_20200929_1152.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0088_shoppinglist_finished.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0089_auto_20201117_2222.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0090_auto_20201214_1359.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0091_auto_20201226_1551.py
  function migrate_empty_units (line 6) | def migrate_empty_units(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0092_recipe_servings.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0093_auto_20201231_1236.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0094_auto_20201231_1238.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0095_auto_20210107_1804.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0096_auto_20210109_2044.py
  function delete_duplicate_bookmarks (line 6) | def delete_duplicate_bookmarks(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0097_auto_20210113_1315.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0098_auto_20210113_1320.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0099_auto_20210113_1518.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0100_recipe_servings_text.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0101_storage_path.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0102_auto_20210125_1147.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0103_food_ignore_shopping.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0104_auto_20210125_2133.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0105_auto_20210126_1604.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0106_shoppinglist_supermarket.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0107_auto_20210128_1535.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0108_auto_20210219_1410.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0109_auto_20210221_1204.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0110_auto_20210221_1406.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0111_space_created_by.py
  function set_default_owner (line 9) | def set_default_owner(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0112_remove_synclog_space.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0113_auto_20210317_2017.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0114_importlog.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0115_telegrambot.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0116_auto_20210319_0012.py
  function remove_empty_food_unit (line 7) | def remove_empty_food_unit(apps, schema_editor):
  class Migration (line 34) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0117_space_max_recipes.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0118_auto_20210406_1805.py
  function migrate_no_group_superusers (line 7) | def migrate_no_group_superusers(apps, schema_editor):
  class Migration (line 17) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0119_auto_20210411_2101.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0120_bookmarklet.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0121_auto_20210518_1638.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0122_auto_20210527_1712.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0123_invitelink_email.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0124_alter_userpreference_theme.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0125_space_demo.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0126_alter_userpreference_theme.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0127_remove_invitelink_username.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0128_userfile.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0129_auto_20210608_1233.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0130_alter_userfile_file_size_kb.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0131_auto_20210608_1929.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0132_sharelink_request_count.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0133_sharelink_abuse_blocked.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0134_space_allow_sharing.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0135_auto_20210615_2210.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0136_auto_20210617_1343.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0137_auto_20210617_1501.py
  function migrate_spaces (line 10) | def migrate_spaces(apps, schema_editor):
  class Migration (line 26) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0138_auto_20210617_1602.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0139_space_created_at.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0140_userpreference_created_at.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0141_auto_20210713_1042.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0142_alter_userpreference_search_style.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0143_build_full_text_index.py
  function allSearchFields (line 15) | def allSearchFields():
  function nameSearchField (line 19) | def nameSearchField():
  function set_default_search_vector (line 23) | def set_default_search_vector(apps, schema_editor):
  class Migration (line 35) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0144_create_searchfields.py
  function create_searchfields (line 5) | def create_searchfields(apps, schema_editor):
  class Migration (line 13) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0145_alter_userpreference_search_style.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0146_alter_userpreference_use_fractions.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0147_keyword_to_tree.py
  function update_paths (line 12) | def update_paths(apps, schema_editor):
  function backwards (line 23) | def backwards(apps, schema_editor):
  class Migration (line 27) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0148_auto_20210813_1829.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0149_fix_leading_trailing_spaces.py
  function update_paths (line 5) | def update_paths(apps, schema_editor):
  function backwards (line 19) | def backwards(apps, schema_editor):
  class Migration (line 23) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0150_food_to_tree.py
  function update_paths (line 12) | def update_paths(apps, schema_editor):
  function backwards (line 23) | def backwards(apps, schema_editor):
  class Migration (line 27) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0151_auto_20210915_1037.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0152_automation.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0153_auto_20210915_2327.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0154_auto_20210922_1705.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0155_mealtype_default.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0156_searchpreference_trigram_threshold.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0157_alter_searchpreference_trigram.py
  function nameSearchField (line 9) | def nameSearchField():
  function add_default_trigram (line 13) | def add_default_trigram(apps, schema_editor):
  class Migration (line 26) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0158_userpreference_use_kj.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0159_add_shoppinglistentry_fields.py
  function copy_values_to_sle (line 12) | def copy_values_to_sle(apps, schema_editor):
  class Migration (line 24) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0160_delete_shoppinglist_orphans.py
  function delete_orphaned_sle (line 11) | def delete_orphaned_sle(apps, schema_editor):
  function create_inheritfields (line 18) | def create_inheritfields(apps, schema_editor):
  function set_completed_at (line 28) | def set_completed_at(apps, schema_editor):
  class Migration (line 37) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0161_alter_shoppinglistentry_food.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0162_userpreference_csv_delim.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0163_auto_20220105_0758.py
  function rename_inherit_field (line 9) | def rename_inherit_field(apps, schema_editor):
  class Migration (line 17) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0164_space_show_facet_count.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0165_remove_step_type.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0166_alter_userpreference_shopping_add_onhand.py
  function add_default_trigram (line 7) | def add_default_trigram(apps, schema_editor):
  class Migration (line 14) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0167_userpreference_left_handed.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0168_add_unit_searchfields.py
  function create_searchfields (line 6) | def create_searchfields(apps, schema_editor):
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0169_exportlog.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0170_auto_20220207_1848.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0171_alter_searchpreference_trigram_threshold.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0172_ingredient_original_text.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0173_recipe_source_url.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0174_alter_food_substitute_userspace.py
  function migrate_space_permissions (line 9) | def migrate_space_permissions(apps, schema_editor):
  class Migration (line 19) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0175_remove_userpreference_space.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0176_alter_searchpreference_icontains_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0177_recipe_show_ingredient_overview.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0178_remove_userpreference_search_style_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0179_recipe_private_recipe_shared.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0180_invitelink_reusable.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0181_space_image.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0182_userpreference_image.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0183_alter_space_image.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0184_alter_userpreference_image.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0185_food_plural_name_ingredient_always_use_plural_food_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0186_automation_order_alter_automation_type.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0187_alter_space_use_plural.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0188_space_no_sharing_limit.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0189_property_propertytype_unitconversion_food_fdc_id_and_more.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0190_auto_20230525_1506.py
  function migrate_old_nutrition_data (line 7) | def migrate_old_nutrition_data(apps, schema_editor):
  class Migration (line 30) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0191_foodproperty_property_import_food_id_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0192_food_food_unique_open_data_slug_per_space_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0193_space_internal_note.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0194_alter_food_properties_food_amount.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0195_invitelink_internal_note_userspace_internal_note_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0196_food_url.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0197_step_show_ingredients_table_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0198_propertytype_order.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0199_alter_propertytype_options_alter_automation_type_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0200_alter_propertytype_options_remove_keyword_icon_and_more.py
  function migrate_icons (line 9) | def migrate_icons(apps, schema_editor):
  class Migration (line 37) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py
  function apply_migration (line 8) | def apply_migration(apps, schema_editor):
  class Migration (line 14) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0202_remove_space_show_facet_count.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0203_alter_unique_contstraints.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0204_propertytype_fdc_id.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0205_alter_food_fdc_id_alter_propertytype_fdc_id.py
  function fix_fdc_ids (line 7) | def fix_fdc_ids(apps, schema_editor):
  class Migration (line 14) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0206_rename_sticky_navbar_userpreference_nav_sticky_and_more.py
  function get_nav_bg_color (line 24) | def get_nav_bg_color(theme, nav_color):
  function get_nav_text_color (line 39) | def get_nav_text_color(theme, nav_color):
  function get_current_colors (line 54) | def get_current_colors(apps, schema_editor):
  class Migration (line 70) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0207_space_logo_color_128_space_logo_color_144_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0208_space_app_name_userpreference_max_owned_spaces.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0209_remove_space_use_plural.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0210_shoppinglistentry_updated_at.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0211_recipebook_order.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0212_alter_property_property_amount.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0213_remove_property_property_unique_import_food_per_space_and_more.py
  function migrate_property_import_slug (line 7) | def migrate_property_import_slug(apps, schema_editor):
  class Migration (line 29) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0214_cooklog_comment_cooklog_updated_at_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0215_connectorconfig.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0216_delete_shoppinglist.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0217_alter_userpreference_default_page.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0218_alter_mealplan_from_date_alter_mealplan_to_date.py
  function timezone_correction (line 8) | def timezone_correction(apps, schema_editor):
  class Migration (line 24) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0219_connectorconfig_supports_description_field.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0220_shoppinglistrecipe_created_by_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0221_migrate_shoppinglistrecipe_space_created_by.py
  function add_space_and_owner_to_shopping_list_recipe (line 10) | def add_space_and_owner_to_shopping_list_recipe(apps, schema_editor):
  class Migration (line 35) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0222_alter_shoppinglistrecipe_created_by_and_more.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0223_auto_20250831_1111.py
  function migrate_comments (line 7) | def migrate_comments(apps, schema_editor):
  class Migration (line 26) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0224_space_ai_credits_balance_space_ai_credits_monthly_and_more.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0225_space_ai_enabled.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0226_aiprovider_log_credit_cost_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0227_space_ai_default_provider_and_more.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0228_space_space_setup_completed.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0229_alter_ailog_options_alter_aiprovider_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0230_auto_20250925_2056.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0231_alter_aiprovider_options_alter_automation_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0232_shoppinglist.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0233_food_shopping_lists_shoppinglistentry_shopping_lists_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0234_alter_shoppinglist_options_and_more.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0235_recipe_diameter_recipe_diameter_text.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0236_household_userspace_household_inventorylocation_and_more.py
  class Migration (line 9) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0237_remove_mealtype_mt_unique_name_per_space_and_more.py
  function apply_migration (line 11) | def apply_migration(apps, schema_editor):
  class Migration (line 47) | class Migration(migrations.Migration):

FILE: cookbook/migrations/0238_auto_20260312_1920.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: cookbook/models.py
  function get_user_display_name (line 30) | def get_user_display_name(self):
  function get_active_space (line 37) | def get_active_space(self):
  function oauth_token_get_owner (line 54) | def oauth_token_get_owner(self):
  function get_model_name (line 61) | def get_model_name(model):
  class TreeManager (line 65) | class TreeManager(MP_NodeManager):
    method create (line 66) | def create(self, *args, **kwargs):
    method get_or_create (line 70) | def get_or_create(self, *args, **kwargs):
  class TreeModel (line 103) | class TreeModel(MP_Node):
    method __str__ (line 106) | def __str__(self):
    method parent (line 110) | def parent(self):
    method full_name (line 117) | def full_name(self) -> str:
    method get_ancestors_and_self (line 125) | def get_ancestors_and_self(self):
    method get_descendants_and_self (line 135) | def get_descendants_and_self(self):
    method has_children (line 143) | def has_children(self):
    method get_num_children (line 146) | def get_num_children(self):
    method add_root (line 151) | def add_root(self, **kwargs):
    method include_descendants (line 157) | def include_descendants(queryset=None, filter=None):
    method exclude_descendants (line 170) | def exclude_descendants(queryset=None, filter=None):
    method include_ancestors (line 182) | def include_ancestors(queryset=None):
    class Meta (line 196) | class Meta:
  class MergeModelMixin (line 200) | class MergeModelMixin:
    method merge_into (line 202) | def merge_into(self, target):
  class PermissionModelMixin (line 221) | class PermissionModelMixin:
    method get_space_key (line 223) | def get_space_key():
    method get_space_kwarg (line 226) | def get_space_kwarg(self):
    method get_owner (line 229) | def get_owner(self):
    method get_shared (line 236) | def get_shared(self):
    method get_space (line 241) | def get_space(self):
  class FoodInheritField (line 250) | class FoodInheritField(models.Model, PermissionModelMixin):
    method __str__ (line 254) | def __str__(self):
    method get_name (line 258) | def get_name(self):
  class Space (line 262) | class Space(ExportModelOperationsMixin('space'), models.Model):
    method safe_delete (line 329) | def safe_delete(self):
    method get_owner (line 387) | def get_owner(self):
    method get_space (line 390) | def get_space(self):
    method __str__ (line 393) | def __str__(self):
    class Meta (line 396) | class Meta:
  class AiProvider (line 400) | class AiProvider(models.Model):
    method __str__ (line 414) | def __str__(self):
    class Meta (line 417) | class Meta:
  class AiLog (line 421) | class AiLog(models.Model, PermissionModelMixin):
    method __str__ (line 443) | def __str__(self):
    class Meta (line 446) | class Meta:
  class ConnectorConfig (line 450) | class ConnectorConfig(models.Model, PermissionModelMixin):
    class Meta (line 474) | class Meta:
  class UserPreference (line 478) | class UserPreference(models.Model, PermissionModelMixin):
    method save (line 553) | def save(self, *args, **kwargs):
    method __str__ (line 563) | def __str__(self):
  class Household (line 567) | class Household(models.Model, PermissionModelMixin):
  class UserSpace (line 576) | class UserSpace(models.Model, PermissionModelMixin):
    class Meta (line 592) | class Meta:
  class Storage (line 596) | class Storage(models.Model, PermissionModelMixin):
    method __str__ (line 616) | def __str__(self):
    class Meta (line 619) | class Meta:
  class Sync (line 623) | class Sync(models.Model, PermissionModelMixin):
    method __str__ (line 634) | def __str__(self):
    class Meta (line 637) | class Meta:
  class SupermarketCategory (line 641) | class SupermarketCategory(models.Model, PermissionModelMixin, MergeModel...
    method __str__ (line 649) | def __str__(self):
    method merge_into (line 652) | def merge_into(self, target):
    class Meta (line 660) | class Meta:
  class Supermarket (line 668) | class Supermarket(models.Model, PermissionModelMixin):
    method __str__ (line 678) | def __str__(self):
    class Meta (line 681) | class Meta:
  class SupermarketCategoryRelation (line 689) | class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
    method get_space_key (line 697) | def get_space_key():
    class Meta (line 700) | class Meta:
  class SyncLog (line 707) | class SyncLog(models.Model, PermissionModelMixin):
    method __str__ (line 715) | def __str__(self):
    class Meta (line 718) | class Meta:
  class Keyword (line 722) | class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, Permissi...
    class Meta (line 733) | class Meta:
  class Unit (line 741) | class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionM...
    method merge_into (line 751) | def merge_into(self, target):
    method __str__ (line 762) | def __str__(self):
    class Meta (line 765) | class Meta:
  class Food (line 773) | class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionMode...
    method __str__ (line 809) | def __str__(self):
    method merge_into (line 812) | def merge_into(self, target):
    method move (line 839) | def move(self, *args, **kwargs):
    method reset_inheritance (line 851) | def reset_inheritance(space=None, food=None):
    class Meta (line 896) | class Meta:
  class UnitConversion (line 908) | class UnitConversion(ExportModelOperationsMixin('unit_conversion'), mode...
    method __str__ (line 924) | def __str__(self):
    class Meta (line 927) | class Meta:
  class Ingredient (line 935) | class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model,...
    class Meta (line 954) | class Meta:
  class Step (line 961) | class Step(ExportModelOperationsMixin('step'), models.Model, PermissionM...
    method get_instruction_render (line 976) | def get_instruction_render(self):
    method __str__ (line 980) | def __str__(self):
    class Meta (line 985) | class Meta:
  class PropertyType (line 990) | class PropertyType(models.Model, PermissionModelMixin, MergeModelMixin):
    method __str__ (line 1019) | def __str__(self):
    method merge_into (line 1022) | def merge_into(self, target):
    class Meta (line 1029) | class Meta:
  class Property (line 1037) | class Property(models.Model, PermissionModelMixin):
    method __str__ (line 1046) | def __str__(self):
    class Meta (line 1049) | class Meta:
  class FoodProperty (line 1055) | class FoodProperty(models.Model):
    class Meta (line 1059) | class Meta:
  class NutritionInformation (line 1065) | class NutritionInformation(models.Model, PermissionModelMixin):
    method __str__ (line 1077) | def __str__(self):
  class RecipeManager (line 1081) | class RecipeManager(models.Manager.from_queryset(models.QuerySet)):
    method get_queryset (line 1082) | def get_queryset(self):
  class Recipe (line 1086) | class Recipe(ExportModelOperationsMixin('recipe'), models.Model, Permiss...
    method __str__ (line 1121) | def __str__(self):
    method get_related_recipes (line 1124) | def get_related_recipes(self, levels=1):
    class Meta (line 1139) | class Meta:
  class Comment (line 1149) | class Comment(ExportModelOperationsMixin('comment'), models.Model, Permi...
    method get_space_key (line 1159) | def get_space_key():
    method get_space (line 1162) | def get_space(self):
    method __str__ (line 1165) | def __str__(self):
    class Meta (line 1168) | class Meta:
  class RecipeImport (line 1172) | class RecipeImport(models.Model, PermissionModelMixin):
    method __str__ (line 1182) | def __str__(self):
    method convert_to_recipe (line 1185) | def convert_to_recipe(self, user):
    class Meta (line 1198) | class Meta:
  class RecipeBook (line 1202) | class RecipeBook(ExportModelOperationsMixin('book'), models.Model, Permi...
    method __str__ (line 1213) | def __str__(self):
    class Meta (line 1216) | class Meta():
  class RecipeBookEntry (line 1221) | class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.M...
    method get_space_key (line 1228) | def get_space_key():
    method __str__ (line 1231) | def __str__(self):
    method get_owner (line 1234) | def get_owner(self):
    class Meta (line 1240) | class Meta:
  class MealType (line 1246) | class MealType(models.Model, PermissionModelMixin):
    method __str__ (line 1257) | def __str__(self):
    class Meta (line 1260) | class Meta:
  class MealPlan (line 1267) | class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Pe...
    method get_label (line 1281) | def get_label(self):
    method get_meal_name (line 1286) | def get_meal_name(self):
    method __str__ (line 1289) | def __str__(self):
    class Meta (line 1292) | class Meta:
  class ShoppingListRecipe (line 1296) | class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recip...
    class Meta (line 1310) | class Meta:
  class ShoppingList (line 1314) | class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.M...
    class Meta (line 1325) | class Meta:
  class ShoppingListEntry (line 1329) | class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'...
    method __str__ (line 1348) | def __str__(self):
    method get_owner (line 1351) | def get_owner(self):
    class Meta (line 1357) | class Meta:
  class InventoryLocation (line 1361) | class InventoryLocation(models.Model, PermissionModelMixin):
  class InventoryEntry (line 1374) | class InventoryEntry(models.Model, PermissionModelMixin):
    class Meta (line 1394) | class Meta:
  class InventoryLog (line 1401) | class InventoryLog(models.Model, PermissionModelMixin):
    class Meta (line 1425) | class Meta:
  class ShareLink (line 1429) | class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, ...
    method __str__ (line 1440) | def __str__(self):
    class Meta (line 1443) | class Meta:
  function default_valid_until (line 1447) | def default_valid_until():
  class InviteLink (line 1451) | class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model...
    method __str__ (line 1466) | def __str__(self):
    class Meta (line 1469) | class Meta:
  class TelegramBot (line 1473) | class TelegramBot(models.Model, PermissionModelMixin):
    method __str__ (line 1483) | def __str__(self):
    class Meta (line 1486) | class Meta:
  class CookLog (line 1490) | class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, Perm...
    method __str__ (line 1503) | def __str__(self):
    class Meta (line 1506) | class Meta:
  class ViewLog (line 1518) | class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, Perm...
    method __str__ (line 1526) | def __str__(self):
    class Meta (line 1529) | class Meta:
  class ImportLog (line 1539) | class ImportLog(models.Model, PermissionModelMixin):
    method __str__ (line 1554) | def __str__(self):
    class Meta (line 1557) | class Meta:
  class ExportLog (line 1561) | class ExportLog(models.Model, PermissionModelMixin):
    method __str__ (line 1577) | def __str__(self):
    class Meta (line 1580) | class Meta:
  class BookmarkletImport (line 1584) | class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import')...
    class Meta (line 1593) | class Meta:
  class SearchFields (line 1599) | class SearchFields(models.Model, PermissionModelMixin):
    method __str__ (line 1603) | def __str__(self):
    method get_name (line 1607) | def get_name(self):
  class SearchPreference (line 1611) | class SearchPreference(models.Model, PermissionModelMixin):
  class UserFile (line 1637) | class UserFile(ExportModelOperationsMixin('user_files'), models.Model, P...
    method is_image (line 1647) | def is_image(self):
    method save (line 1654) | def save(self, *args, **kwargs):
    method __str__ (line 1660) | def __str__(self):
    class Meta (line 1663) | class Meta:
  class Automation (line 1667) | class Automation(ExportModelOperationsMixin('automations'), models.Model...
    class Meta (line 1712) | class Meta:
  class CustomFilter (line 1716) | class CustomFilter(models.Model, PermissionModelMixin):
    method __str__ (line 1738) | def __str__(self):
    class Meta (line 1741) | class Meta:

FILE: cookbook/provider/dropbox.py
  class Dropbox (line 11) | class Dropbox(Provider):
    method import_all (line 14) | def import_all(monitor):
    method create_share_link (line 63) | def create_share_link(recipe):
    method get_share_link (line 80) | def get_share_link(recipe):
    method get_file (line 102) | def get_file(recipe):
    method rename_file (line 114) | def rename_file(recipe, new_name):
    method delete_file (line 136) | def delete_file(recipe):

FILE: cookbook/provider/local.py
  class Local (line 12) | class Local(Provider):
    method import_all (line 15) | def import_all(monitor):
    method get_file (line 49) | def get_file(recipe):
    method is_path_allowed (line 58) | def is_path_allowed(path):
    method rename_file (line 67) | def rename_file(recipe, new_name):
    method delete_file (line 75) | def delete_file(recipe):

FILE: cookbook/provider/nextcloud.py
  class Nextcloud (line 15) | class Nextcloud(Provider):
    method get_client (line 18) | def get_client(storage):
    method import_all (line 30) | def import_all(monitor):
    method create_share_link (line 70) | def create_share_link(recipe):
    method get_share_link (line 87) | def get_share_link(recipe):
    method get_file (line 111) | def get_file(recipe):
    method rename_file (line 127) | def rename_file(recipe, new_name):
    method delete_file (line 142) | def delete_file(recipe):

FILE: cookbook/provider/provider.py
  class Provider (line 1) | class Provider:
    method import_all (line 3) | def import_all(monitor):
    method create_share_link (line 7) | def create_share_link(recipe):
    method get_share_link (line 11) | def get_share_link(recipe):
    method get_file (line 15) | def get_file(recipe):
    method rename_file (line 19) | def rename_file(recipe, new_name):
    method delete_file (line 23) | def delete_file(recipe):

FILE: cookbook/serializer.py
  class WritableNestedModelSerializer (line 48) | class WritableNestedModelSerializer(WNMS):
    method to_internal_value (line 51) | def to_internal_value(self, data):
  class ExtendedRecipeMixin (line 75) | class ExtendedRecipeMixin(serializers.ModelSerializer):
    method get_fields (line 85) | def get_fields(self, *args, **kwargs):
    method get_image (line 104) | def get_image(self, obj):
  class OpenDataModelMixin (line 114) | class OpenDataModelMixin(serializers.ModelSerializer):
    method create (line 116) | def create(self, validated_data):
    method update (line 121) | def update(self, instance, validated_data):
  class CustomDecimalField (line 128) | class CustomDecimalField(serializers.Field):
    method to_representation (line 134) | def to_representation(self, value):
    method to_internal_value (line 139) | def to_internal_value(self, data):
  class CustomOnHandField (line 152) | class CustomOnHandField(serializers.Field):
    method get_attribute (line 153) | def get_attribute(self, instance):
    method to_representation (line 156) | def to_representation(self, obj):
    method to_internal_value (line 174) | def to_internal_value(self, data):
  class SpaceFilterSerializer (line 178) | class SpaceFilterSerializer(serializers.ListSerializer):
    method to_representation (line 179) | def to_representation(self, data):
  class UserSerializer (line 226) | class UserSerializer(WritableNestedModelSerializer):
    method get_user_label (line 230) | def get_user_label(self, obj):
    class Meta (line 233) | class Meta:
  class GroupSerializer (line 240) | class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
    method create (line 241) | def create(self, validated_data):
    method update (line 244) | def update(self, instance, validated_data):
    class Meta (line 247) | class Meta:
  class FoodInheritFieldSerializer (line 253) | class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelS...
    method create (line 257) | def create(self, validated_data):
    method update (line 260) | def update(self, instance, validated_data):
    class Meta (line 263) | class Meta:
  class UserFileSerializer (line 269) | class UserFileSerializer(serializers.ModelSerializer):
    method get_download_link (line 276) | def get_download_link(self, obj):
    method get_preview_link (line 280) | def get_preview_link(self, obj):
    method check_file_limit (line 288) | def check_file_limit(self, validated_data):
    method check_file_type (line 304) | def check_file_type(self, validated_data):
    method create (line 312) | def create(self, validated_data):
    method update (line 319) | def update(self, instance, validated_data):
    class Meta (line 324) | class Meta:
  class UserFileViewSerializer (line 331) | class UserFileViewSerializer(serializers.ModelSerializer):
    method get_download_link (line 337) | def get_download_link(self, obj):
    method get_preview_link (line 341) | def get_preview_link(self, obj):
    method create (line 349) | def create(self, validated_data):
    method update (line 352) | def update(self, instance, validated_data):
    class Meta (line 355) | class Meta:
  class AiProviderSerializer (line 361) | class AiProviderSerializer(serializers.ModelSerializer):
    method create (line 364) | def create(self, validated_data):
    method update (line 369) | def update(self, instance, validated_data):
    method handle_global_space_logic (line 373) | def handle_global_space_logic(self, validated_data, instance=None):
    class Meta (line 393) | class Meta:
  class AiLogSerializer (line 399) | class AiLogSerializer(serializers.ModelSerializer):
    class Meta (line 402) | class Meta:
  class SpaceSerializer (line 409) | class SpaceSerializer(WritableNestedModelSerializer):
    method get_user_count (line 429) | def get_user_count(self, obj):
    method get_recipe_count (line 433) | def get_recipe_count(self, obj):
    method get_ai_monthly_credits_used (line 437) | def get_ai_monthly_credits_used(self, obj):
    method get_file_size_mb (line 441) | def get_file_size_mb(self, obj):
    method create (line 447) | def create(self, validated_data):
    method update (line 458) | def update(self, instance, validated_data):
    method filter_superuser_parameters (line 467) | def filter_superuser_parameters(self, validated_data):
    class Meta (line 479) | class Meta:
  class HouseholdSerializer (line 492) | class HouseholdSerializer(WritableNestedModelSerializer):
    method create (line 494) | def create(self, validated_data):
    class Meta (line 498) | class Meta:
  class UserSpaceSerializer (line 504) | class UserSpaceSerializer(WritableNestedModelSerializer):
    method create (line 509) | def create(self, validated_data):
    class Meta (line 512) | class Meta:
  class SpacedModelSerializer (line 518) | class SpacedModelSerializer(serializers.ModelSerializer):
    method create (line 519) | def create(self, validated_data):
  class ShoppingListSerializer (line 524) | class ShoppingListSerializer(SpacedModelSerializer, WritableNestedModelS...
    method create (line 526) | def create(self, validated_data):
    class Meta (line 532) | class Meta:
  class MealTypeSerializer (line 538) | class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSeria...
    method create (line 540) | def create(self, validated_data):
    class Meta (line 547) | class Meta:
  class UserPreferenceSerializer (line 554) | class UserPreferenceSerializer(WritableNestedModelSerializer):
    method get_food_inherit_defaults (line 564) | def get_food_inherit_defaults(self, obj):
    method get_food_children_exist (line 568) | def get_food_children_exist(self, obj):
    method update (line 572) | def update(self, instance, validated_data):
    method create (line 576) | def create(self, validated_data):
    class Meta (line 579) | class Meta:
  class SearchFieldsSerializer (line 595) | class SearchFieldsSerializer(UniqueFieldsMixin, WritableNestedModelSeria...
    method create (line 599) | def create(self, validated_data):
    method update (line 602) | def update(self, instance, validated_data):
    class Meta (line 605) | class Meta:
  class SearchPreferenceSerializer (line 611) | class SearchPreferenceSerializer(WritableNestedModelSerializer):
    method create (line 620) | def create(self, validated_data):
    class Meta (line 623) | class Meta:
  class ConnectorConfigSerializer (line 629) | class ConnectorConfigSerializer(SpacedModelSerializer):
    method create (line 631) | def create(self, validated_data):
    class Meta (line 635) | class Meta:
  class StorageSerializer (line 650) | class StorageSerializer(WritableNestedModelSerializer, SpacedModelSerial...
    method create (line 652) | def create(self, validated_data):
    class Meta (line 656) | class Meta:
  class RecipeImportSerializer (line 671) | class RecipeImportSerializer(WritableNestedModelSerializer, SpacedModelS...
    class Meta (line 674) | class Meta:
  class SyncSerializer (line 679) | class SyncSerializer(WritableNestedModelSerializer, SpacedModelSerializer):
    class Meta (line 682) | class Meta:
  class SyncLogSerializer (line 690) | class SyncLogSerializer(SpacedModelSerializer):
    class Meta (line 693) | class Meta:
  class KeywordLabelSerializer (line 698) | class KeywordLabelSerializer(serializers.ModelSerializer):
    method get_label (line 702) | def get_label(self, obj):
    class Meta (line 705) | class Meta:
  class KeywordSerializer (line 712) | class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
    method get_label (line 719) | def get_label(self, obj):
    method create (line 722) | def create(self, validated_data):
    class Meta (line 730) | class Meta:
  class UnitSerializer (line 738) | class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataMod...
    method create (line 741) | def create(self, validated_data):
    method update (line 758) | def update(self, instance, validated_data):
    class Meta (line 764) | class Meta:
  class SupermarketCategorySerializer (line 770) | class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedMod...
    method create (line 772) | def create(self, validated_data):
    method update (line 779) | def update(self, instance, validated_data):
    class Meta (line 782) | class Meta:
  class SupermarketCategoryRelationSerializer (line 787) | class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
    class Meta (line 790) | class Meta:
  class SupermarketSerializer (line 795) | class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, Wr...
    method create (line 799) | def create(self, validated_data):
    class Meta (line 806) | class Meta:
  class PropertyTypeSerializer (line 811) | class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSeri...
    method create (line 815) | def create(self, validated_data):
    class Meta (line 822) | class Meta:
  class PropertySerializer (line 827) | class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
    method create (line 831) | def create(self, validated_data):
    class Meta (line 835) | class Meta:
  class RecipeSimpleSerializer (line 840) | class RecipeSimpleSerializer(WritableNestedModelSerializer):
    method get_url (line 844) | def get_url(self, obj):
    method create (line 847) | def create(self, validated_data):
    method update (line 851) | def update(self, instance, validated_data):
    class Meta (line 855) | class Meta:
  class RecipeFlatSerializer (line 860) | class RecipeFlatSerializer(WritableNestedModelSerializer):
    method create (line 862) | def create(self, validated_data):
    method update (line 866) | def update(self, instance, validated_data):
    class Meta (line 870) | class Meta:
  class FoodSimpleSerializer (line 876) | class FoodSimpleSerializer(serializers.ModelSerializer):
    class Meta (line 877) | class Meta:
  class FoodSerializer (line 882) | class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, E...
    method get_substitute_onhand (line 901) | def get_substitute_onhand(self, obj):
    method create (line 927) | def create(self, validated_data):
    method update (line 976) | def update(self, instance, validated_data):
    class Meta (line 1003) | class Meta:
  class IngredientSimpleSerializer (line 1013) | class IngredientSimpleSerializer(WritableNestedModelSerializer):
    method create (line 1019) | def create(self, validated_data):
    method update (line 1023) | def update(self, instance, validated_data):
    class Meta (line 1027) | class Meta:
  class IngredientSerializer (line 1036) | class IngredientSerializer(IngredientSimpleSerializer):
    method get_used_in_recipes (line 1042) | def get_used_in_recipes(self, obj):
    method get_conversions (line 1050) | def get_conversions(self, obj):
    class Meta (line 1061) | class Meta:
  class StepSerializer (line 1071) | class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
    method create (line 1078) | def create(self, validated_data):
    method get_instructions_markdown (line 1083) | def get_instructions_markdown(self, obj):
    method get_step_recipes (line 1087) | def get_step_recipes(self, obj):
    method get_step_recipe_data (line 1092) | def get_step_recipe_data(self, obj):
    class Meta (line 1098) | class Meta:
  class StepRecipeSerializer (line 1106) | class StepRecipeSerializer(WritableNestedModelSerializer):
    class Meta (line 1109) | class Meta:
  class UnitConversionSerializer (line 1114) | class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataMo...
    method get_conversion_name (line 1123) | def get_conversion_name(self, obj):
    method create (line 1129) | def create(self, validated_data):
    class Meta (line 1142) | class Meta:
  class NutritionInformationSerializer (line 1147) | class NutritionInformationSerializer(serializers.ModelSerializer):
    method create (line 1153) | def create(self, validated_data):
    class Meta (line 1157) | class Meta:
  class RecipeBaseSerializer (line 1162) | class RecipeBaseSerializer(WritableNestedModelSerializer):
    method is_recipe_new (line 1165) | def is_recipe_new(self, obj):
  class CommentSerializer (line 1172) | class CommentSerializer(serializers.ModelSerializer):
    class Meta (line 1173) | class Meta:
  class RecipeOverviewSerializer (line 1179) | class RecipeOverviewSerializer(RecipeBaseSerializer):
    method create (line 1187) | def create(self, validated_data):
    method update (line 1190) | def update(self, instance, validated_data):
    class Meta (line 1193) | class Meta:
  class RecipeSerializer (line 1210) | class RecipeSerializer(RecipeBaseSerializer):
    method get_food_properties (line 1222) | def get_food_properties(self, obj):
    class Meta (line 1233) | class Meta:
    method validate (line 1242) | def validate(self, data):
    method create (line 1248) | def create(self, validated_data):
  class RecipeImageSerializer (line 1254) | class RecipeImageSerializer(WritableNestedModelSerializer):
    method create (line 1258) | def create(self, validated_data):
    method update (line 1263) | def update(self, instance, validated_data):
    class Meta (line 1268) | class Meta:
  class RecipeBatchUpdateSerializer (line 1273) | class RecipeBatchUpdateSerializer(serializers.Serializer):
  class FoodBatchUpdateSerializer (line 1295) | class FoodBatchUpdateSerializer(serializers.Serializer):
  class CustomFilterSerializer (line 1329) | class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelS...
    method create (line 1332) | def create(self, validated_data):
    class Meta (line 1336) | class Meta:
  class RecipeBookSerializer (line 1342) | class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSer...
    method create (line 1347) | def create(self, validated_data):
    class Meta (line 1351) | class Meta:
  class RecipeBookEntrySerializer (line 1357) | class RecipeBookEntrySerializer(serializers.ModelSerializer):
    method get_book_content (line 1362) | def get_book_content(self, obj):
    method get_recipe_content (line 1366) | def get_recipe_content(self, obj):
    method create (line 1369) | def create(self, validated_data):
    class Meta (line 1377) | class Meta:
  class MealPlanSerializer (line 1382) | class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSeria...
    method get_note_markdown (line 1396) | def get_note_markdown(self, obj):
    method in_shopping (line 1400) | def in_shopping(self, obj):
    method _apply_default_time (line 1404) | def _apply_default_time(dt, meal_type_obj):
    method create (line 1418) | def create(self, validated_data):
    method update (line 1449) | def update(self, obj, validated_data):
    class Meta (line 1456) | class Meta:
  class AutoMealPlanSerializer (line 1466) | class AutoMealPlanSerializer(serializers.Serializer):
  class ShoppingListRecipeSerializer (line 1476) | class ShoppingListRecipeSerializer(serializers.ModelSerializer):
    method create (line 1482) | def create(self, validated_data):
    method update (line 1487) | def update(self, instance, validated_data):
    class Meta (line 1493) | class Meta:
  class FoodShoppingSerializer (line 1499) | class FoodShoppingSerializer(serializers.ModelSerializer):
    method create (line 1504) | def create(self, validated_data):
    class Meta (line 1531) | class Meta:
  class ShoppingListEntrySerializer (line 1536) | class ShoppingListEntrySerializer(WritableNestedModelSerializer):
    method get_fields (line 1547) | def get_fields(self, *args, **kwargs):
    method run_validation (line 1556) | def run_validation(self, data):
    method create (line 1576) | def create(self, validated_data):
    method update (line 1596) | def update(self, instance, validated_data):
    class Meta (line 1611) | class Meta:
  class ShoppingListEntrySimpleCreateSerializer (line 1620) | class ShoppingListEntrySimpleCreateSerializer(serializers.Serializer):
  class ShoppingListEntryBulkCreateSerializer (line 1627) | class ShoppingListEntryBulkCreateSerializer(serializers.Serializer):
  class ShoppingListEntryBulkSerializer (line 1632) | class ShoppingListEntryBulkSerializer(serializers.Serializer):
  class ShoppingListEntryCheckedSerializer (line 1644) | class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
    class Meta (line 1645) | class Meta:
  class ShareLinkSerializer (line 1650) | class ShareLinkSerializer(SpacedModelSerializer):
    class Meta (line 1651) | class Meta:
  class CookLogSerializer (line 1656) | class CookLogSerializer(serializers.ModelSerializer):
    method create (line 1659) | def create(self, validated_data):
    class Meta (line 1664) | class Meta:
  class ViewLogSerializer (line 1670) | class ViewLogSerializer(serializers.ModelSerializer):
    method create (line 1671) | def create(self, validated_data):
    class Meta (line 1682) | class Meta:
  class ImportLogSerializer (line 1688) | class ImportLogSerializer(serializers.ModelSerializer):
    method create (line 1691) | def create(self, validated_data):
    class Meta (line 1696) | class Meta:
  class ExportLogSerializer (line 1703) | class ExportLogSerializer(serializers.ModelSerializer):
    method create (line 1705) | def create(self, validated_data):
    class Meta (line 1710) | class Meta:
  class AutomationSerializer (line 1719) | class AutomationSerializer(serializers.ModelSerializer):
    method create (line 1721) | def create(self, validated_data):
    class Meta (line 1726) | class Meta:
  class InventoryLocationSerializer (line 1733) | class InventoryLocationSerializer(UniqueFieldsMixin, SpacedModelSerializ...
    method create (line 1736) | def create(self, validated_data):
    class Meta (line 1741) | class Meta:
  class InventoryEntrySerializer (line 1746) | class InventoryEntrySerializer(SpacedModelSerializer, WritableNestedMode...
    method get_label (line 1752) | def get_label(self, obj):
    method create (line 1759) | def create(self, validated_data):
    method update (line 1781) | def update(self, instance, validated_data):
    class Meta (line 1801) | class Meta:
  class InventoryLogSerializer (line 1810) | class InventoryLogSerializer(SpacedModelSerializer):
    method create (line 1815) | def create(self, validated_data):
    method update (line 1818) | def update(self, instance, validated_data):
    class Meta (line 1821) | class Meta:
  class InviteLinkSerializer (line 1826) | class InviteLinkSerializer(WritableNestedModelSerializer):
    method get_email_sent (line 1831) | def get_email_sent(self, obj):
    method create (line 1835) | def create(self, validated_data):
    class Meta (line 1873) | class Meta:
  class BookmarkletImportListSerializer (line 1884) | class BookmarkletImportListSerializer(serializers.ModelSerializer):
    method create (line 1885) | def create(self, validated_data):
    class Meta (line 1890) | class Meta:
  class BookmarkletImportSerializer (line 1896) | class BookmarkletImportSerializer(BookmarkletImportListSerializer):
    class Meta (line 1897) | class Meta:
  class AccessTokenSerializer (line 1905) | class AccessTokenSerializer(serializers.ModelSerializer):
    method create (line 1908) | def create(self, validated_data):
    method get_token (line 1914) | def get_token(self, obj):
    class Meta (line 1922) | class Meta:
  class LocalizationSerializer (line 1928) | class LocalizationSerializer(serializers.Serializer):
    class Meta (line 1932) | class Meta:
  class ServerSettingsSerializer (line 1936) | class ServerSettingsSerializer(serializers.Serializer):
    class Meta (line 1962) | class Meta:
  class FdcQueryFoodsSerializer (line 1967) | class FdcQueryFoodsSerializer(serializers.Serializer):
  class FdcQuerySerializer (line 1973) | class FdcQuerySerializer(serializers.Serializer):
  class GenericModelReferenceSerializer (line 1980) | class GenericModelReferenceSerializer(serializers.Serializer):
  class KeywordExportSerializer (line 1988) | class KeywordExportSerializer(KeywordSerializer):
    class Meta (line 1989) | class Meta:
  class NutritionInformationExportSerializer (line 1994) | class NutritionInformationExportSerializer(NutritionInformationSerializer):
    class Meta (line 1995) | class Meta:
  class SupermarketCategoryExportSerializer (line 2000) | class SupermarketCategoryExportSerializer(SupermarketCategorySerializer):
    class Meta (line 2001) | class Meta:
  class UnitExportSerializer (line 2006) | class UnitExportSerializer(UnitSerializer):
    class Meta (line 2007) | class Meta:
  class FoodExportSerializer (line 2012) | class FoodExportSerializer(FoodSerializer):
    class Meta (line 2015) | class Meta:
  class IngredientExportSerializer (line 2020) | class IngredientExportSerializer(WritableNestedModelSerializer):
    method create (line 2025) | def create(self, validated_data):
    class Meta (line 2029) | class Meta:
  class StepExportSerializer (line 2035) | class StepExportSerializer(WritableNestedModelSerializer):
    method create (line 2038) | def create(self, validated_data):
    class Meta (line 2042) | class Meta:
  class RecipeExportSerializer (line 2047) | class RecipeExportSerializer(WritableNestedModelSerializer):
    class Meta (line 2052) | class Meta:
    method create (line 2059) | def create(self, validated_data):
  class RecipeShoppingUpdateSerializer (line 2065) | class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
    class Meta (line 2073) | class Meta:
  class FoodShoppingUpdateSerializer (line 2078) | class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
    class Meta (line 2086) | class Meta:
  class RecipeFromSourceSerializer (line 2093) | class RecipeFromSourceSerializer(serializers.Serializer):
  class SourceImportFoodSerializer (line 2099) | class SourceImportFoodSerializer(serializers.Serializer):
  class SourceImportUnitSerializer (line 2103) | class SourceImportUnitSerializer(serializers.Serializer):
  class SourceImportIngredientSerializer (line 2107) | class SourceImportIngredientSerializer(serializers.Serializer):
  class SourceImportStepSerializer (line 2115) | class SourceImportStepSerializer(serializers.Serializer):
  class SourceImportKeywordSerializer (line 2121) | class SourceImportKeywordSerializer(serializers.Serializer):
  class SourceImportPropertyTypeSerializer (line 2128) | class SourceImportPropertyTypeSerializer(serializers.Serializer):
  class SourceImportPropertySerializer (line 2133) | class SourceImportPropertySerializer(serializers.Serializer):
  class SourceImportRecipeSerializer (line 2138) | class SourceImportRecipeSerializer(serializers.Serializer):
  class SourceImportDuplicateSerializer (line 2154) | class SourceImportDuplicateSerializer(serializers.Serializer):
  class RecipeFromSourceResponseSerializer (line 2159) | class RecipeFromSourceResponseSerializer(serializers.Serializer):
  class AiImportSerializer (line 2168) | class AiImportSerializer(serializers.Serializer):
  class ExportRequestSerializer (line 2175) | class ExportRequestSerializer(serializers.Serializer):
  class ImportOpenDataSerializer (line 2182) | class ImportOpenDataSerializer(serializers.Serializer):
  class ImportOpenDataResponseDetailSerializer (line 2189) | class ImportOpenDataResponseDetailSerializer(serializers.Serializer):
  class ImportOpenDataResponseSerializer (line 2196) | class ImportOpenDataResponseSerializer(serializers.Serializer):
  class ImportOpenDataVersionMetaDataSerializer (line 2205) | class ImportOpenDataVersionMetaDataSerializer(serializers.Serializer):
  class ImportOpenDataMetaDataSerializer (line 2214) | class ImportOpenDataMetaDataSerializer(serializers.Serializer):
  class IngredientParserRequestSerializer (line 2238) | class IngredientParserRequestSerializer(serializers.Serializer):
  class IngredientParserResponseSerializer (line 2243) | class IngredientParserResponseSerializer(serializers.Serializer):

FILE: cookbook/signals.py
  function skip_signal (line 25) | def skip_signal(signal_func):
  function create_user_preference (line 38) | def create_user_preference(sender, instance=None, created=False, **kwargs):
  function create_search_preference (line 45) | def create_search_preference(sender, instance=None, created=False, **kwa...
  function update_recipe_search_vector (line 55) | def update_recipe_search_vector(sender, instance=None, created=False, **...
  function update_step_search_vector (line 71) | def update_step_search_vector(sender, instance=None, created=False, **kw...
  function update_food_inheritance (line 85) | def update_food_inheritance(sender, instance=None, created=False, **kwar...
  function clear_unit_cache (line 125) | def clear_unit_cache(sender, instance=None, created=False, **kwargs):
  function clear_property_type_cache (line 133) | def clear_property_type_cache(sender, instance=None, created=False, **kw...

FILE: cookbook/static/pdfjs/web/debugger.mjs
  function removeSelection (line 27) | function removeSelection() {
  function resetSelection (line 33) | function resetSelection() {
  function selectFont (line 39) | function selectFont(fontName, show) {
  function textLayerClick (line 47) | function textLayerClick(e) {
  method init (line 71) | init() {
  method cleanup (line 81) | cleanup() {
  method active (line 85) | get active() {
  method active (line 88) | set active(value) {
  method fontAdded (line 99) | fontAdded(fontObj, url) {
  method init (line 180) | init() {
  method cleanup (line 194) | cleanup() {
  method create (line 202) | create(pageIndex) {
  method selectStepper (line 220) | selectStepper(pageIndex, selectPanel) {
  method saveBreakPoints (line 232) | saveBreakPoints(pageIndex, bps) {
  class Stepper (line 240) | class Stepper {
    method #c (line 242) | #c(tag, textContent) {
    method #simplifyArgs (line 250) | #simplifyArgs(args) {
    method constructor (line 280) | constructor(panel, pageIndex, initialBreakPoints) {
    method init (line 291) | init(operatorList) {
    method updateOperatorList (line 310) | updateOperatorList(operatorList) {
    method getNextBreakPoint (line 400) | getNextBreakPoint() {
    method breakIt (line 410) | breakIt(idx, callback) {
    method goTo (line 434) | goTo(idx) {
  function clear (line 449) | function clear(node) {
  function getStatIndex (line 452) | function getStatIndex(pageNumber) {
  method init (line 466) | init() {}
  method add (line 470) | add(pageNumber, stat) {
  method cleanup (line 494) | cleanup() {
  class PDFBug (line 502) | class PDFBug {
    method enable (line 509) | static enable(ids) {
    method init (line 529) | static init(container, ids) {
    method loadCSS (line 580) | static loadCSS() {
    method cleanup (line 590) | static cleanup() {
    method selectPanel (line 598) | static selectPanel(index) {

FILE: cookbook/static/pdfjs/web/pdf.mjs
  constant IDENTITY_MATRIX (line 103) | const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
  constant FONT_IDENTITY_MATRIX (line 104) | const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
  constant LINE_FACTOR (line 105) | const LINE_FACTOR = 1.35;
  constant LINE_DESCENT_FACTOR (line 106) | const LINE_DESCENT_FACTOR = 0.35;
  constant BASELINE_FACTOR (line 107) | const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR;
  constant OPS (line 282) | const OPS = {
  function setVerbosityLevel (line 385) | function setVerbosityLevel(level) {
  function getVerbosityLevel (line 390) | function getVerbosityLevel() {
  function info (line 393) | function info(msg) {
  function warn (line 398) | function warn(msg) {
  function unreachable (line 403) | function unreachable(msg) {
  function assert (line 406) | function assert(cond, msg) {
  function _isValidProtocol (line 411) | function _isValidProtocol(url) {
  function createValidAbsoluteUrl (line 423) | function createValidAbsoluteUrl(url, baseUrl = null, options = null) {
  function shadow (line 443) | function shadow(obj, prop, value, nonSerializable = false) {
  function BaseException (line 453) | function BaseException(message, name) {
  class PasswordException (line 461) | class PasswordException extends BaseException {
    method constructor (line 462) | constructor(msg, code) {
  class UnknownErrorException (line 467) | class UnknownErrorException extends BaseException {
    method constructor (line 468) | constructor(msg, details) {
  class InvalidPDFException (line 473) | class InvalidPDFException extends BaseException {
    method constructor (line 474) | constructor(msg) {
  class ResponseException (line 478) | class ResponseException extends BaseException {
    method constructor (line 479) | constructor(msg, status, missing) {
  class FormatError (line 485) | class FormatError extends BaseException {
    method constructor (line 486) | constructor(msg) {
  class AbortException (line 490) | class AbortException extends BaseException {
    method constructor (line 491) | constructor(msg) {
  function bytesToString (line 495) | function bytesToString(bytes) {
  function stringToBytes (line 512) | function stringToBytes(str) {
  function string32 (line 523) | function string32(value) {
  function objectSize (line 526) | function objectSize(obj) {
  function objectFromMap (line 529) | function objectFromMap(map) {
  function isLittleEndian (line 536) | function isLittleEndian() {
  function isEvalSupported (line 542) | function isEvalSupported() {
  class util_FeatureTest (line 550) | class util_FeatureTest {
    method isLittleEndian (line 551) | static get isLittleEndian() {
    method isEvalSupported (line 554) | static get isEvalSupported() {
    method isOffscreenCanvasSupported (line 557) | static get isOffscreenCanvasSupported() {
    method isImageDecoderSupported (line 560) | static get isImageDecoderSupported() {
    method platform (line 563) | static get platform() {
    method isCSSRoundSupported (line 585) | static get isCSSRoundSupported() {
  class Util (line 590) | class Util {
    method makeHexColor (line 591) | static makeHexColor(r, g, b) {
    method transform (line 594) | static transform(m1, m2) {
    method applyTransform (line 597) | static applyTransform(p, m) {
    method applyInverseTransform (line 602) | static applyInverseTransform(p, m) {
    method getAxialAlignedBoundingBox (line 608) | static getAxialAlignedBoundingBox(r, m) {
    method inverseTransform (line 615) | static inverseTransform(m) {
    method singularValueDecompose2dScale (line 619) | static singularValueDecompose2dScale(m) {
    method normalizeRect (line 631) | static normalizeRect(rect) {
    method intersect (line 643) | static intersect(rect1, rect2) {
    method pointBoundingBox (line 656) | static pointBoundingBox(x, y, minMax) {
    method rectBoundingBox (line 662) | static rectBoundingBox(x0, y0, x1, y1, minMax) {
    method #getExtremumOnCurve (line 668) | static #getExtremumOnCurve(x0, x1, x2, x3, y0, y1, y2, y3, t, minMax) {
    method #getExtremum (line 682) | static #getExtremum(x0, x1, x2, x3, y0, y1, y2, y3, a, b, c, minMax) {
    method bezierBoundingBox (line 698) | static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
  function stringToPDFString (line 708) | function stringToPDFString(str) {
  function stringToUTF8String (line 752) | function stringToUTF8String(str) {
  function utf8StringToString (line 755) | function utf8StringToString(str) {
  function isArrayEqual (line 758) | function isArrayEqual(arr1, arr2) {
  function getModificationDate (line 769) | function getModificationDate(date = new Date()) {
  function normalizeUnicode (line 775) | function normalizeUnicode(str) {
  function getUuid (line 782) | function getUuid() {
  function _isValidExplicitDest (line 791) | function _isValidExplicitDest(validRef, validName, dest) {
  function MathClamp (line 838) | function MathClamp(v, min, max) {
  function toHexUtil (line 841) | function toHexUtil(arr) {
  function toBase64Util (line 847) | function toBase64Util(arr) {
  function fromBase64Util (line 853) | function fromBase64Util(str) {
  constant SVG_NS (line 874) | const SVG_NS = "http://www.w3.org/2000/svg";
  class PixelsPerInch (line 875) | class PixelsPerInch {
  function fetchData (line 880) | async function fetchData(url, type = "text") {
  class PageViewport (line 920) | class PageViewport {
    method constructor (line 921) | constructor({
    method rawDims (line 993) | get rawDims() {
    method clone (line 1002) | clone({
    method convertToViewportPoint (line 1019) | convertToViewportPoint(x, y) {
    method convertToViewportRectangle (line 1022) | convertToViewportRectangle(rect) {
    method convertToPdfPoint (line 1027) | convertToPdfPoint(x, y) {
  class RenderingCancelledException (line 1031) | class RenderingCancelledException extends BaseException {
    method constructor (line 1032) | constructor(msg, extraDelay = 0) {
  function isDataScheme (line 1037) | function isDataScheme(url) {
  function isPdfFile (line 1045) | function isPdfFile(filename) {
  function getFilenameFromUrl (line 1048) | function getFilenameFromUrl(url) {
  function getPdfFilenameFromUrl (line 1052) | function getPdfFilenameFromUrl(url, defaultFilename = "document.pdf") {
  class StatTimer (line 1074) | class StatTimer {
    method time (line 1077) | time(name) {
    method timeEnd (line 1083) | timeEnd(name) {
    method toString (line 1094) | toString() {
  function isValidFetchUrl (line 1112) | function isValidFetchUrl(url, baseUrl) {
  function noContextMenu (line 1116) | function noContextMenu(e) {
  function stopEvent (line 1119) | function stopEvent(e) {
  function deprecated (line 1123) | function deprecated(details) {
  class PDFDateString (line 1126) | class PDFDateString {
    method toDateObject (line 1128) | static toDateObject(input) {
  function getXfaPageViewport (line 1163) | function getXfaPageViewport(xfaPage, {
  function getRGB (line 1179) | function getRGB(color) {
  function getColorValues (line 1193) | function getColorValues(colors) {
  function getCurrentTransform (line 1204) | function getCurrentTransform(ctx) {
  function getCurrentTransformInverse (line 1215) | function getCurrentTransformInverse(ctx) {
  function setLayerDimensions (line 1226) | function setLayerDimensions(div, viewport, mustFlip = false, mustRotate ...
  class OutputScale (line 1252) | class OutputScale {
    method constructor (line 1253) | constructor() {
    method scaled (line 1260) | get scaled() {
    method symmetric (line 1263) | get symmetric() {
    method limitCanvas (line 1266) | limitCanvas(width, height, maxPixels, maxDim) {
    method pixelRatio (line 1285) | static get pixelRatio() {
  class EditorToolbar (line 1293) | class EditorToolbar {
    method constructor (line 1301) | constructor(editor) {
    method render (line 1311) | render() {
    method div (line 1337) | get div() {
    method #pointerDown (line 1340) | static #pointerDown(e) {
    method #focusIn (line 1343) | #focusIn(e) {
    method #focusOut (line 1347) | #focusOut(e) {
    method #addListenersToElement (line 1351) | #addListenersToElement(element) {
    method hide (line 1365) | hide() {
    method show (line 1369) | show() {
    method #addDeleteButton (line 1373) | #addDeleteButton() {
    method #divider (line 1390) | get #divider() {
    method addAltText (line 1395) | async addAltText(altText) {
    method addColorPicker (line 1401) | addColorPicker(colorPicker) {
    method addEditSignatureButton (line 1407) | async addEditSignatureButton(signatureManager) {
    method updateEditSignatureButton (line 1412) | updateEditSignatureButton(description) {
    method remove (line 1417) | remove() {
  class HighlightToolbar (line 1423) | class HighlightToolbar {
    method constructor (line 1427) | constructor(uiManager) {
    method #render (line 1430) | #render() {
    method #getLastPoint (line 1443) | #getLastPoint(boxes, isLTR) {
    method show (line 1467) | show(parent, boxes, isLTR) {
    method hide (line 1476) | hide() {
    method #addHighlightButton (line 1479) | #addHighlightButton() {
  function bindEvents (line 1505) | function bindEvents(obj, element, names) {
  class IdManager (line 1510) | class IdManager {
    method id (line 1512) | get id() {
  class ImageManager (line 1516) | class ImageManager {
    method _isSVGFittingCanvas (line 1520) | static get _isSVGFittingCanvas() {
    method #get (line 1534) | async #get(key, rawData) {
    method getFromFile (line 1592) | async getFromFile(file) {
    method getFromUrl (line 1601) | async getFromUrl(url) {
    method getFromBlob (line 1604) | async getFromBlob(id, blobPromise) {
    method getFromId (line 1608) | async getFromId(id) {
    method getFromCanvas (line 1630) | getFromCanvas(id, canvas) {
    method getSvgUrl (line 1650) | getSvgUrl(id) {
    method deleteId (line 1657) | deleteId(id) {
    method isValidId (line 1679) | isValidId(id) {
  class CommandManager (line 1683) | class CommandManager {
    method constructor (line 1688) | constructor(maxSize = 128) {
    method add (line 1691) | add({
    method undo (line 1738) | undo() {
    method redo (line 1752) | redo() {
    method hasSomethingToUndo (line 1765) | hasSomethingToUndo() {
    method hasSomethingToRedo (line 1768) | hasSomethingToRedo() {
    method cleanType (line 1771) | cleanType(type) {
    method destroy (line 1785) | destroy() {
  class KeyboardManager (line 1789) | class KeyboardManager {
    method constructor (line 1790) | constructor(callbacks) {
    method #serialize (line 1816) | #serialize(event) {
    method exec (line 1834) | exec(self, event) {
  class ColorManager (line 1859) | class ColorManager {
    method _colors (line 1861) | get _colors() {
    method convert (line 1866) | convert(color) {
    method getHexCode (line 1878) | getHexCode(name) {
  class AnnotationEditorUIManager (line 1886) | class AnnotationEditorUIManager {
    method _keyboardManager (line 1941) | static get _keyboardManager() {
    method constructor (line 1999) | constructor(container, viewer, altTextManager, signatureManager, event...
    method destroy (line 2043) | destroy() {
    method combinedSignal (line 2075) | combinedSignal(ac) {
    method mlManager (line 2078) | get mlManager() {
    method useNewAltTextFlow (line 2081) | get useNewAltTextFlow() {
    method useNewAltTextWhenAddingImage (line 2084) | get useNewAltTextWhenAddingImage() {
    method hcmFilter (line 2087) | get hcmFilter() {
    method direction (line 2090) | get direction() {
    method highlightColors (line 2093) | get highlightColors() {
    method highlightColorNames (line 2096) | get highlightColorNames() {
    method setCurrentDrawingSession (line 2099) | setCurrentDrawingSession(layer) {
    method setMainHighlightColorPicker (line 2108) | setMainHighlightColorPicker(colorPicker) {
    method editAltText (line 2111) | editAltText(editor, firstTime = false) {
    method getSignature (line 2114) | getSignature(editor) {
    method signatureManager (line 2120) | get signatureManager() {
    method switchToMode (line 2123) | switchToMode(mode, callback) {
    method setPreference (line 2133) | setPreference(name, value) {
    method onSetPreference (line 2140) | onSetPreference({
    method onPageChanging (line 2150) | onPageChanging({
    method focusMainContainer (line 2155) | focusMainContainer() {
    method findParent (line 2158) | findParent(x, y) {
    method disableUserSelect (line 2172) | disableUserSelect(value = false) {
    method addShouldRescale (line 2175) | addShouldRescale(editor) {
    method removeShouldRescale (line 2178) | removeShouldRescale(editor) {
    method onScaleChanging (line 2181) | onScaleChanging({
    method onRotationChanging (line 2191) | onRotationChanging({
    method #getAnchorElementForSelection (line 2197) | #getAnchorElementForSelection({
    method #getLayerForTextLayer (line 2202) | #getLayerForTextLayer(textLayer) {
    method highlightSelection (line 2216) | highlightSelection(methodOfCreation = "") {
    method #displayHighlightToolbar (line 2260) | #displayHighlightToolbar() {
    method addToAnnotationStorage (line 2274) | addToAnnotationStorage(editor) {
    method #selectionChange (line 2279) | #selectionChange() {
    method #onSelectEnd (line 2344) | #onSelectEnd(methodOfCreation = "") {
    method #addSelectionListener (line 2351) | #addSelectionListener() {
    method #addFocusManager (line 2356) | #addFocusManager() {
    method #removeFocusManager (line 2369) | #removeFocusManager() {
    method blur (line 2373) | blur() {
    method focus (line 2393) | focus() {
    method #addKeyboardManager (line 2407) | #addKeyboardManager() {
    method #removeKeyboardManager (line 2420) | #removeKeyboardManager() {
    method #addCopyPasteListeners (line 2424) | #addCopyPasteListeners() {
    method #removeCopyPasteListeners (line 2440) | #removeCopyPasteListeners() {
    method #addDragAndDropListeners (line 2444) | #addDragAndDropListeners() {
    method addEditListeners (line 2453) | addEditListeners() {
    method removeEditListeners (line 2457) | removeEditListeners() {
    method dragOver (line 2461) | dragOver(event) {
    method drop (line 2474) | drop(event) {
    method copy (line 2485) | copy(event) {
    method cut (line 2503) | cut(event) {
    method paste (line 2507) | async paste(event) {
    method keydown (line 2564) | keydown(event) {
    method keyup (line 2572) | keyup(event) {
    method onEditingAction (line 2581) | onEditingAction({
    method #dispatchUpdateStates (line 2596) | #dispatchUpdateStates(details) {
    method #dispatchUpdateUI (line 2608) | #dispatchUpdateUI(details) {
    method setEditingState (line 2614) | setEditingState(isEditing) {
    method registerEditorTypes (line 2634) | registerEditorTypes(types) {
    method getId (line 2643) | getId() {
    method currentLayer (line 2646) | get currentLayer() {
    method getLayer (line 2649) | getLayer(pageIndex) {
    method currentPageIndex (line 2652) | get currentPageIndex() {
    method addLayer (line 2655) | addLayer(layer) {
    method removeLayer (line 2663) | removeLayer(layer) {
    method updateMode (line 2666) | async updateMode(mode, editId = null, isFromKeyboard = false) {
    method addNewEditorFromKeyboard (line 2711) | addNewEditorFromKeyboard() {
    method updateToolbar (line 2716) | updateToolbar(mode) {
    method updateParams (line 2725) | updateParams(type, value) {
    method showAllEditors (line 2758) | showAllEditors(type, visible, updateButton = false) {
    method enableWaiting (line 2769) | enableWaiting(mustWait = false) {
    method #enableAll (line 2783) | async #enableAll() {
    method #disableAll (line 2796) | #disableAll() {
    method getEditors (line 2808) | getEditors(pageIndex) {
    method getEditor (line 2817) | getEditor(id) {
    method addEditor (line 2820) | addEditor(editor) {
    method removeEditor (line 2823) | removeEditor(editor) {
    method addDeletedAnnotationElement (line 2842) | addDeletedAnnotationElement(editor) {
    method isDeletedAnnotationElement (line 2847) | isDeletedAnnotationElement(annotationElementId) {
    method removeDeletedAnnotationElement (line 2850) | removeDeletedAnnotationElement(editor) {
    method #addEditorToLayer (line 2855) | #addEditorToLayer(editor) {
    method setActiveEditor (line 2864) | setActiveEditor(editor) {
    method #lastSelectedEditor (line 2873) | get #lastSelectedEditor() {
    method updateUI (line 2878) | updateUI(editor) {
    method updateUIForDefaultProperties (line 2883) | updateUIForDefaultProperties(editorType) {
    method toggleSelected (line 2886) | toggleSelected(editor) {
    method setSelected (line 2902) | setSelected(editor) {
    method isSelected (line 2917) | isSelected(editor) {
    method firstSelectedEditor (line 2920) | get firstSelectedEditor() {
    method unselect (line 2923) | unselect(editor) {
    method hasSelection (line 2930) | get hasSelection() {
    method isEnterHandled (line 2933) | get isEnterHandled() {
    method undo (line 2936) | undo() {
    method redo (line 2945) | redo() {
    method addCommands (line 2953) | addCommands(params) {
    method cleanUndoStack (line 2961) | cleanUndoStack(type) {
    method #isEmpty (line 2964) | #isEmpty() {
    method delete (line 2975) | delete() {
    method commitOrRemove (line 2999) | commitOrRemove() {
    method hasSomethingToControl (line 3002) | hasSomethingToControl() {
    method #selectEditors (line 3005) | #selectEditors(editors) {
    method selectAll (line 3021) | selectAll() {
    method unselectAll (line 3027) | unselectAll() {
    method translateSelectedEditors (line 3048) | translateSelectedEditors(x, y, noCommit = false) {
    method setUpDragSession (line 3091) | setUpDragSession() {
    method endDragSession (line 3108) | endDragSession() {
    method dragSelectedEditors (line 3164) | dragSelectedEditors(tx, ty) {
    method rebuild (line 3172) | rebuild(editor) {
    method isEditorHandlingKeyboard (line 3187) | get isEditorHandlingKeyboard() {
    method isActive (line 3190) | isActive(editor) {
    method getActive (line 3193) | getActive() {
    method getMode (line 3196) | getMode() {
    method imageManager (line 3199) | get imageManager() {
    method getSelectionBoxes (line 3202) | getSelectionBoxes(textLayer) {
    method addChangedExistingAnnotation (line 3273) | addChangedExistingAnnotation({
    method removeChangedExistingAnnotation (line 3279) | removeChangedExistingAnnotation({
    method renderAnnotationElement (line 3284) | renderAnnotationElement(annotation) {
    method setMissingCanvas (line 3298) | setMissingCanvas(annotationId, annotationElementId, canvas) {
    method addMissingCanvas (line 3306) | addMissingCanvas(annotationId, editor) {
  class AltText (line 3313) | class AltText {
    method constructor (line 3328) | constructor(editor) {
    method initialize (line 3340) | static initialize(l10n) {
    method render (line 3343) | async render() {
    method #label (line 3391) | get #label() {
    method finish (line 3394) | finish() {
    method isEmpty (line 3403) | isEmpty() {
    method hasData (line 3409) | hasData() {
    method guessedText (line 3415) | get guessedText() {
    method setGuessedText (line 3418) | async setGuessedText(guessedText) {
    method toggleAltTextBadge (line 3428) | toggleAltTextBadge(visibility = false) {
    method serialize (line 3441) | serialize(isForCopying) {
    method data (line 3453) | get data() {
    method data (line 3459) | set data({
    method toggle (line 3479) | toggle(enabled = false) {
    method shown (line 3489) | shown() {
    method destroy (line 3497) | destroy() {
    method #setState (line 3505) | async #setState() {
  class TouchManager (line 3578) | class TouchManager {
    method constructor (line 3591) | constructor({
    method MIN_TOUCH_DISTANCE_TO_PINCH (line 3613) | get MIN_TOUCH_DISTANCE_TO_PINCH() {
    method #onTouchStart (line 3616) | #onTouchStart(evt) {
    method #onTouchMove (line 3684) | #onTouchMove(evt) {
    method #onTouchEnd (line 3728) | #onTouchEnd(evt) {
    method destroy (line 3742) | destroy() {
  class AnnotationEditor (line 3757) | class AnnotationEditor {
    method _resizerKeyboardManager (line 3795) | static get _resizerKeyboardManager() {
    method constructor (line 3817) | constructor(parameters) {
    method editorType (line 3848) | get editorType() {
    method isDrawer (line 3851) | static get isDrawer() {
    method _defaultLineColor (line 3854) | static get _defaultLineColor() {
    method deleteAnnotationElement (line 3857) | static deleteAnnotationElement(editor) {
    method initialize (line 3867) | static initialize(l10n, _uiManager) {
    method updateDefaultParams (line 3885) | static updateDefaultParams(_type, _value) {}
    method defaultPropertiesToUpdate (line 3886) | static get defaultPropertiesToUpdate() {
    method isHandlingMimeForPasting (line 3889) | static isHandlingMimeForPasting(mime) {
    method paste (line 3892) | static paste(item, parent) {
    method propertiesToUpdate (line 3895) | get propertiesToUpdate() {
    method _isDraggable (line 3898) | get _isDraggable() {
    method _isDraggable (line 3901) | set _isDraggable(value) {
    method isEnterHandled (line 3905) | get isEnterHandled() {
    method center (line 3908) | center() {
    method addCommands (line 3930) | addCommands(params) {
    method currentLayer (line 3933) | get currentLayer() {
    method setInBackground (line 3936) | setInBackground() {
    method setInForeground (line 3939) | setInForeground() {
    method setParent (line 3942) | setParent(parent) {
    method focusin (line 3951) | focusin(event) {
    method focusout (line 3961) | focusout(event) {
    method commitOrRemove (line 3977) | commitOrRemove() {
    method commit (line 3984) | commit() {
    method addToAnnotationStorage (line 3987) | addToAnnotationStorage() {
    method setAt (line 3990) | setAt(x, y, tx, ty) {
    method _moveAfterPaste (line 3997) | _moveAfterPaste(baseX, baseY) {
    method #translate (line 4002) | #translate([width, height], x, y) {
    method translate (line 4009) | translate(x, y) {
    method translateInPage (line 4012) | translateInPage(x, y) {
    method translationDone (line 4019) | translationDone() {
    method drag (line 4022) | drag(tx, ty) {
    method _onTranslating (line 4057) | _onTranslating(x, y) {}
    method _onTranslated (line 4058) | _onTranslated(x, y) {}
    method _hasBeenMoved (line 4059) | get _hasBeenMoved() {
    method _hasBeenResized (line 4062) | get _hasBeenResized() {
    method getBaseTranslation (line 4065) | getBaseTranslation() {
    method _mustFixPosition (line 4083) | get _mustFixPosition() {
    method fixAndSetPosition (line 4086) | fixAndSetPosition(rotation = this.rotation) {
    method #rotatePoint (line 4132) | static #rotatePoint(x, y, angle) {
    method screenToPageTranslation (line 4144) | screenToPageTranslation(x, y) {
    method pageTranslationToScreen (line 4147) | pageTranslationToScreen(x, y) {
    method #getRotationMatrix (line 4150) | #getRotationMatrix(rotation) {
    method parentScale (line 4168) | get parentScale() {
    method parentRotation (line 4171) | get parentRotation() {
    method parentDimensions (line 4174) | get parentDimensions() {
    method setDims (line 4181) | setDims(width, height) {
    method fixDims (line 4191) | fixDims() {
    method getInitialTranslation (line 4212) | getInitialTranslation() {
    method #createResizers (line 4215) | #createResizers() {
    method #resizerPointerdown (line 4238) | #resizerPointerdown(name, event) {
    method #resize (line 4290) | #resize(x, y, width, height) {
    method _onResized (line 4300) | _onResized() {}
    method #addResizeToUndoStack (line 4301) | #addResizeToUndoStack() {
    method _round (line 4325) | static _round(x) {
    method #resizerPointermove (line 4328) | #resizerPointermove(name, event) {
    method _onResizing (line 4430) | _onResizing() {}
    method altTextFinish (line 4431) | altTextFinish() {
    method addEditToolbar (line 4434) | async addEditToolbar() {
    method removeEditToolbar (line 4445) | removeEditToolbar() {
    method addContainer (line 4453) | addContainer(container) {
    method getClientDimensions (line 4461) | getClientDimensions() {
    method addAltTextButton (line 4464) | async addAltTextButton() {
    method altTextData (line 4476) | get altTextData() {
    method altTextData (line 4479) | set altTextData(data) {
    method guessedAltText (line 4485) | get guessedAltText() {
    method setGuessedAltText (line 4488) | async setGuessedAltText(text) {
    method serializeAltText (line 4491) | serializeAltText(isForCopying) {
    method hasAltText (line 4494) | hasAltText() {
    method hasAltTextData (line 4497) | hasAltTextData() {
    method render (line 4500) | render() {
    method #touchPinchStartCallback (line 4536) | #touchPinchStartCallback() {
    method #touchPinchCallback (line 4546) | #touchPinchCallback(_origin, prevDistance, distance) {
    method #touchPinchEndCallback (line 4580) | #touchPinchEndCallback() {
    method pointerdown (line 4585) | pointerdown(event) {
    method isSelected (line 4600) | get isSelected() {
    method #selectOnPointerEvent (line 4603) | #selectOnPointerEvent(event) {
    method #setUpDragSession (line 4613) | #setUpDragSession(event) {
    method _onStartDragging (line 4685) | _onStartDragging() {}
    method _onStopDragging (line 4686) | _onStopDragging() {}
    method moveInDOM (line 4687) | moveInDOM() {
    method _setParentAndPosition (line 4696) | _setParentAndPosition(parent, x, y) {
    method getRect (line 4703) | getRect(tx, ty, rotation = this.rotation) {
    method getRectInCurrentCoords (line 4726) | getRectInCurrentCoords(rect, pageHeight) {
    method onceAdded (line 4743) | onceAdded(focus) {}
    method isEmpty (line 4744) | isEmpty() {
    method enableEditMode (line 4747) | enableEditMode() {
    method disableEditMode (line 4750) | disableEditMode() {
    method isInEditMode (line 4753) | isInEditMode() {
    method shouldGetKeyboardEvents (line 4756) | shouldGetKeyboardEvents() {
    method needsToBeRebuilt (line 4759) | needsToBeRebuilt() {
    method isOnScreen (line 4762) | get isOnScreen() {
    method #addFocusListeners (line 4775) | #addFocusListeners() {
    method rebuild (line 4788) | rebuild() {
    method rotate (line 4791) | rotate(_angle) {}
    method resize (line 4792) | resize() {}
    method serializeDeleted (line 4793) | serializeDeleted() {
    method serialize (line 4801) | serialize(isForCopying = false, context = null) {
    method deserialize (line 4804) | static async deserialize(data, parent, uiManager) {
    method hasBeenModified (line 4821) | get hasBeenModified() {
    method remove (line 4824) | remove() {
    method isResizable (line 4851) | get isResizable() {
    method makeResizable (line 4854) | makeResizable() {
    method toolbarPosition (line 4860) | get toolbarPosition() {
    method keydown (line 4863) | keydown(event) {
    method #resizerKeydown (line 4929) | #resizerKeydown(event) {
    method #resizerBlur (line 4932) | #resizerBlur(event) {
    method #resizerFocus (line 4937) | #resizerFocus(name) {
    method #setResizerTabIndex (line 4940) | #setResizerTabIndex(value) {
    method _resizeWithKeyboard (line 4948) | _resizeWithKeyboard(x, y) {
    method #stopResizing (line 4958) | #stopResizing() {
    method _stopResizingWithKeyboard (line 4963) | _stopResizingWithKeyboard() {
    method select (line 4967) | select() {
    method unselect (line 4981) | unselect() {
    method updateParams (line 4992) | updateParams(type, value) {}
    method disableEditing (line 4993) | disableEditing() {}
    method enableEditing (line 4994) | enableEditing() {}
    method enterInEditMode (line 4995) | enterInEditMode() {}
    method getElementForAltText (line 4996) | getElementForAltText() {
    method contentDiv (line 4999) | get contentDiv() {
    method isEditing (line 5002) | get isEditing() {
    method isEditing (line 5005) | set isEditing(value) {
    method setAspectRatio (line 5017) | setAspectRatio(width, height) {
    method MIN_SIZE (line 5026) | static get MIN_SIZE() {
    method canCreateNewEmptyEditor (line 5029) | static canCreateNewEmptyEditor() {
    method telemetryInitialData (line 5032) | get telemetryInitialData() {
    method telemetryFinalData (line 5037) | get telemetryFinalData() {
    method _reportTelemetry (line 5040) | _reportTelemetry(data, mustWait = false) {
    method show (line 5069) | show(visible = this._isVisible) {
    method enable (line 5073) | enable() {
    method disable (line 5079) | disable() {
    method renderAnnotationElement (line 5085) | renderAnnotationElement(annotation) {
    method resetAnnotationElement (line 5099) | resetAnnotationElement(annotation) {
  class FakeEditor (line 5108) | class FakeEditor extends AnnotationEditor {
    method constructor (line 5109) | constructor(params) {
    method serialize (line 5114) | serialize() {
  constant SEED (line 5120) | const SEED = 0xc3d2e1f0;
  constant MASK_HIGH (line 5121) | const MASK_HIGH = 0xffff0000;
  constant MASK_LOW (line 5122) | const MASK_LOW = 0xffff;
  class MurmurHash3_64 (line 5123) | class MurmurHash3_64 {
    method constructor (line 5124) | constructor(seed) {
    method update (line 5128) | update(input) {
    method hexdigest (line 5198) | hexdigest() {
  class AnnotationStorage (line 5221) | class AnnotationStorage {
    method constructor (line 5225) | constructor() {
    method getValue (line 5230) | getValue(key, defaultValue) {
    method getRawValue (line 5237) | getRawValue(key) {
    method remove (line 5240) | remove(key) {
    method setValue (line 5254) | setValue(key, value) {
    method has (line 5275) | has(key) {
    method getAll (line 5278) | getAll() {
    method setAll (line 5281) | setAll(obj) {
    method size (line 5286) | get size() {
    method #setModified (line 5289) | #setModified() {
    method resetModified (line 5297) | resetModified() {
    method print (line 5305) | get print() {
    method serializable (line 5308) | get serializable() {
    method editorStats (line 5338) | get editorStats() {
    method resetModifiedIds (line 5375) | resetModifiedIds() {
    method modifiedIds (line 5378) | get modifiedIds() {
  class PrintAnnotationStorage (line 5395) | class PrintAnnotationStorage extends AnnotationStorage {
    method constructor (line 5397) | constructor(parent) {
    method print (line 5413) | get print() {
    method serializable (line 5416) | get serializable() {
    method modifiedIds (line 5419) | get modifiedIds() {
  class FontLoader (line 5429) | class FontLoader {
    method constructor (line 5431) | constructor({
    method addNativeFontFace (line 5441) | addNativeFontFace(nativeFontFace) {
    method removeNativeFontFace (line 5445) | removeNativeFontFace(nativeFontFace) {
    method insertRule (line 5449) | insertRule(rule) {
    method clear (line 5457) | clear() {
    method loadSystemFont (line 5468) | async loadSystemFont({
    method bind (line 5497) | async bind(font) {
    method isFontLoadingAPISupported (line 5532) | get isFontLoadingAPISupported() {
    method isSyncFontLoadingSupported (line 5536) | get isSyncFontLoadingSupported() {
    method _queueLoadingCallback (line 5539) | _queueLoadingCallback(callback) {
    method _loadTestFont (line 5559) | get _loadTestFont() {
    method _prepareFontLoadEvent (line 5563) | _prepareFontLoadEvent(font, request) {
  class FontFaceObject (line 5628) | class FontFaceObject {
    method constructor (line 5629) | constructor(translatedData, inspectFont = null) {
    method createNativeFontFace (line 5636) | createNativeFontFace() {
    method createFontFaceRule (line 5655) | createFontFaceRule() {
    method getPathGenerator (line 5673) | getPathGenerator(objs, character) {
  function onFn (line 5708) | function onFn() {}
  function wrapReason (line 5709) | function wrapReason(ex) {
  class MessageHandler (line 5730) | class MessageHandler {
    method constructor (line 5732) | constructor(sourceName, targetName, comObj) {
    method #onMessage (line 5746) | #onMessage({
    method on (line 5805) | on(actionName, handler) {
    method send (line 5812) | send(actionName, data, transfers) {
    method sendWithPromise (line 5820) | sendWithPromise(actionName, data, transfers) {
    method sendWithStream (line 5837) | sendWithStream(actionName, data, queueingStrategy, transfers) {
    method #createStreamSink (line 5890) | #createStreamSink(data) {
    method #processStreamMessage (line 5971) | #processStreamMessage(data) {
    method #deleteStreamController (line 6085) | async #deleteStreamController(streamController, streamId) {
    method destroy (line 6089) | destroy() {
  class BaseCanvasFactory (line 6097) | class BaseCanvasFactory {
    method constructor (line 6099) | constructor({
    method create (line 6104) | create(width, height) {
    method reset (line 6116) | reset(canvasAndContext, width, height) {
    method destroy (line 6126) | destroy(canvasAndContext) {
    method _createCanvas (line 6135) | _createCanvas(width, height) {
  class DOMCanvasFactory (line 6139) | class DOMCanvasFactory extends BaseCanvasFactory {
    method constructor (line 6140) | constructor({
    method _createCanvas (line 6149) | _createCanvas(width, height) {
  class BaseCMapReaderFactory (line 6160) | class BaseCMapReaderFactory {
    method constructor (line 6161) | constructor({
    method fetch (line 6168) | async fetch({
    method _fetch (line 6185) | async _fetch(url) {
  class DOMCMapReaderFactory (line 6189) | class DOMCMapReaderFactory extends BaseCMapReaderFactory {
    method _fetch (line 6190) | async _fetch(url) {
  class BaseFilterFactory (line 6199) | class BaseFilterFactory {
    method addFilter (line 6200) | addFilter(maps) {
    method addHCMFilter (line 6203) | addHCMFilter(fgColor, bgColor) {
    method addAlphaFilter (line 6206) | addAlphaFilter(map) {
    method addLuminosityFilter (line 6209) | addLuminosityFilter(map) {
    method addHighlightHCMFilter (line 6212) | addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgC...
    method destroy (line 6215) | destroy(keepHCM = false) {}
  class DOMFilterFactory (line 6217) | class DOMFilterFactory extends BaseFilterFactory {
    method constructor (line 6225) | constructor({
    method #cache (line 6233) | get #cache() {
    method #hcmCache (line 6236) | get #hcmCache() {
    method #defs (line 6239) | get #defs() {
    method #createTables (line 6261) | #createTables(maps) {
    method #createUrl (line 6282) | #createUrl(id) {
    method addFilter (line 6296) | addFilter(maps) {
    method addHCMFilter (line 6319) | addHCMFilter(fgColor, bgColor) {
    method addAlphaFilter (line 6373) | addAlphaFilter(map) {
    method addLuminosityFilter (line 6393) | addLuminosityFilter(map) {
    method addHighlightHCMFilter (line 6421) | addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgC...
    method destroy (line 6477) | destroy(keepHCM = false) {
    method #addLuminosityConversion (line 6489) | #addLuminosityConversion(filter) {
    method #addGrayConversion (line 6495) | #addGrayConversion(filter) {
    method #createFilter (line 6501) | #createFilter(id) {
    method #appendFeFunc (line 6508) | #appendFeFunc(feComponentTransfer, func, table) {
    method #addTransferMapConversion (line 6514) | #addTransferMapConversion(rTable, gTable, bTable, filter) {
    method #addTransferMapAlphaConversion (line 6521) | #addTransferMapAlphaConversion(aTable, filter) {
    method #getRGB (line 6526) | #getRGB(color) {
  class BaseStandardFontDataFactory (line 6535) | class BaseStandardFontDataFactory {
    method constructor (line 6536) | constructor({
    method fetch (line 6541) | async fetch({
    method _fetch (line 6555) | async _fetch(url) {
  class DOMStandardFontDataFactory (line 6559) | class DOMStandardFontDataFactory extends BaseStandardFontDataFactory {
    method _fetch (line 6560) | async _fetch(url) {
  class BaseWasmFactory (line 6569) | class BaseWasmFactory {
    method constructor (line 6570) | constructor({
    method fetch (line 6575) | async fetch({
    method _fetch (line 6589) | async _fetch(url) {
  class DOMWasmFactory (line 6593) | class DOMWasmFactory extends BaseWasmFactory {
    method _fetch (line 6594) | async _fetch(url) {
  function node_utils_fetchData (line 6610) | async function node_utils_fetchData(url) {
  class NodeFilterFactory (line 6615) | class NodeFilterFactory extends BaseFilterFactory {}
  class NodeCanvasFactory (line 6616) | class NodeCanvasFactory extends BaseCanvasFactory {
    method _createCanvas (line 6617) | _createCanvas(width, height) {
  class NodeCMapReaderFactory (line 6623) | class NodeCMapReaderFactory extends BaseCMapReaderFactory {
    method _fetch (line 6624) | async _fetch(url) {
  class NodeStandardFontDataFactory (line 6628) | class NodeStandardFontDataFactory extends BaseStandardFontDataFactory {
    method _fetch (line 6629) | async _fetch(url) {
  class NodeWasmFactory (line 6633) | class NodeWasmFactory extends BaseWasmFactory {
    method _fetch (line 6634) | async _fetch(url) {
  function applyBoundingBox (line 6647) | function applyBoundingBox(ctx, bbox) {
  class BaseShadingPattern (line 6657) | class BaseShadingPattern {
    method isModifyingCurrentTransform (line 6658) | isModifyingCurrentTransform() {
    method getPattern (line 6661) | getPattern() {
  class RadialAxialShadingPattern (line 6665) | class RadialAxialShadingPattern extends BaseShadingPattern {
    method constructor (line 6666) | constructor(IR) {
    method _createGradient (line 6677) | _createGradient(ctx) {
    method getPattern (line 6689) | getPattern(ctx, owner, inverse, pathType) {
  function drawTriangle (line 6719) | function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) {
  function drawFigure (line 6821) | function drawFigure(data, figure, context) {
  class MeshShadingPattern (line 6847) | class MeshShadingPattern extends BaseShadingPattern {
    method constructor (line 6848) | constructor(IR) {
    method _createMeshCanvas (line 6858) | _createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) {
    method isModifyingCurrentTransform (line 6905) | isModifyingCurrentTransform() {
    method getPattern (line 6908) | getPattern(ctx, owner, inverse, pathType) {
  class DummyShadingPattern (line 6932) | class DummyShadingPattern extends BaseShadingPattern {
    method getPattern (line 6933) | getPattern() {
  function getShadingPattern (line 6937) | function getShadingPattern(IR) {
  class TilingPattern (line 6952) | class TilingPattern {
    method constructor (line 6954) | constructor(IR, ctx, canvasGraphicsFactory, baseTransform) {
    method createPatternCanvas (line 6967) | createPatternCanvas(owner) {
    method getSizeAndScale (line 7063) | getSizeAndScale(step, realOutputSize, scale) {
    method clipBbox (line 7076) | clipBbox(graphics, x0, y0, x1, y1) {
    method setFillAndStrokeStyleToContext (line 7084) | setFillAndStrokeStyleToContext(graphics, paintType, color) {
    method isModifyingCurrentTransform (line 7106) | isModifyingCurrentTransform() {
    method getPattern (line 7109) | getPattern(ctx, owner, inverse, pathType) {
  function convertToRGBA (line 7129) | function convertToRGBA(params) {
  function convertBlackAndWhiteToRGBA (line 7138) | function convertBlackAndWhiteToRGBA({
  function convertRGBToRGBA (line 7179) | function convertRGBToRGBA({
  function grayToRGBA (line 7223) | function grayToRGBA(src, dest) {
  constant MIN_FONT_SIZE (line 7240) | const MIN_FONT_SIZE = 16;
  constant MAX_FONT_SIZE (line 7241) | const MAX_FONT_SIZE = 100;
  constant EXECUTION_TIME (line 7242) | const EXECUTION_TIME = 15;
  constant EXECUTION_STEPS (line 7243) | const EXECUTION_STEPS = 10;
  constant MAX_SIZE_TO_COMPILE (line 7244) | const MAX_SIZE_TO_COMPILE = 1000;
  constant FULL_CHUNK_HEIGHT (line 7245) | const FULL_CHUNK_HEIGHT = 16;
  constant SCALE_MATRIX (line 7246) | const SCALE_MATRIX = new DOMMatrix();
  function mirrorContextOperations (line 7247) | function mirrorContextOperations(ctx, destCtx) {
  class CachedCanvases (line 7345) | class CachedCanvases {
    method constructor (line 7346) | constructor(canvasFactory) {
    method getCanvas (line 7350) | getCanvas(id, width, height) {
    method delete (line 7361) | delete(id) {
    method clear (line 7364) | clear() {
  function drawImageAtIntegerCoords (line 7372) | function drawImageAtIntegerCoords(ctx, srcImg, srcX, srcY, srcW, srcH, d...
  function compileType3Glyph (line 7407) | function compileType3Glyph(imgData) {
  class CanvasExtraState (line 7539) | class CanvasExtraState {
    method constructor (line 7540) | constructor(width, height) {
    method clone (line 7568) | clone() {
    method updateRectMinMax (line 7573) | updateRectMinMax(transform, rect) {
    method getPathBoundingBox (line 7583) | getPathBoundingBox(pathType = PathType.FILL, transform = null) {
    method updateClipFromPath (line 7599) | updateClipFromPath() {
    method isEmptyClip (line 7603) | isEmptyClip() {
    method startNewPathAndClipBox (line 7606) | startNewPathAndClipBox(box) {
    method getClippedPathBoundingBox (line 7613) | getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) {
  function putBinaryImageData (line 7617) | function putBinaryImageData(ctx, imgData) {
  function putBinaryImageMask (line 7710) | function putBinaryImageMask(ctx, imgData) {
  function copyCtxState (line 7739) | function copyCtxState(sourceCtx, destCtx) {
  function resetCtxToDefault (line 7751) | function resetCtxToDefault(ctx) {
  function getImageSmoothingEnabled (line 7774) | function getImageSmoothingEnabled(transform, interpolate) {
  constant LINE_CAP_STYLES (line 7784) | const LINE_CAP_STYLES = ["butt", "round", "square"];
  constant LINE_JOIN_STYLES (line 7785) | const LINE_JOIN_STYLES = ["miter", "round", "bevel"];
  constant NORMAL_CLIP (line 7786) | const NORMAL_CLIP = {};
  constant EO_CLIP (line 7787) | const EO_CLIP = {};
  class CanvasGraphics (line 7788) | class CanvasGraphics {
    method constructor (line 7789) | constructor(canvasCtx, commonObjs, objs, canvasFactory, filterFactory, {
    method getObject (line 7827) | getObject(data, fallback = null) {
    method beginDrawing (line 7833) | beginDrawing({
    method executeOperatorList (line 7864) | executeOperatorList(operatorList, executionStartIdx, continueCallback,...
    method #restoreInitialState (line 7908) | #restoreInitialState() {
    method endDrawing (line 7923) | endDrawing() {
    method #drawFilter (line 7938) | #drawFilter() {
    method _scaleImage (line 7949) | _scaleImage(img, inverseTransform) {
    method _createMaskCanvas (line 7984) | _createMaskCanvas(img) {
    method setLineWidth (line 8052) | setLineWidth(width) {
    method setLineCap (line 8059) | setLineCap(style) {
    method setLineJoin (line 8062) | setLineJoin(style) {
    method setMiterLimit (line 8065) | setMiterLimit(limit) {
    method setDash (line 8068) | setDash(dashArray, dashPhase) {
    method setRenderingIntent (line 8075) | setRenderingIntent(intent) {}
    method setFlatness (line 8076) | setFlatness(flatness) {}
    method setGState (line 8077) | setGState(states) {
    method inSMaskMode (line 8124) | get inSMaskMode() {
    method checkSMaskState (line 8127) | checkSMaskState() {
    method beginSMaskMode (line 8135) | beginSMaskMode() {
    method endSMaskMode (line 8150) | endSMaskMode() {
    method compose (line 8159) | compose(dirtyBox) {
    method composeSMask (line 8179) | composeSMask(ctx, smask, layerCtx, layerBox) {
    method genericComposeSMask (line 8195) | genericComposeSMask(maskCtx, layerCtx, width, height, subtype, backdro...
    method save (line 8239) | save() {
    method restore (line 8248) | restore() {
    method transform (line 8265) | transform(a, b, c, d, e, f) {
    method constructPath (line 8270) | constructPath(op, data, minMax) {
    method closePath (line 8303) | closePath() {
    method stroke (line 8306) | stroke(path, consumePath = true) {
    method closeStroke (line 8331) | closeStroke(path) {
    method fill (line 8334) | fill(path, consumePath = true) {
    method eoFill (line 8366) | eoFill(path) {
    method fillStroke (line 8370) | fillStroke(path) {
    method eoFillStroke (line 8375) | eoFillStroke(path) {
    method closeFillStroke (line 8379) | closeFillStroke(path) {
    method closeEOFillStroke (line 8382) | closeEOFillStroke(path) {
    method endPath (line 8386) | endPath(path) {
    method clip (line 8389) | clip() {
    method eoClip (line 8392) | eoClip() {
    method beginText (line 8395) | beginText() {
    method endText (line 8401) | endText() {
    method setCharSpacing (line 8423) | setCharSpacing(spacing) {
    method setWordSpacing (line 8426) | setWordSpacing(spacing) {
    method setHScale (line 8429) | setHScale(scale) {
    method setLeading (line 8432) | setLeading(leading) {
    method setFont (line 8435) | setFont(fontRefName, size) {
    method setTextRenderingMode (line 8474) | setTextRenderingMode(mode) {
    method setTextRise (line 8477) | setTextRise(rise) {
    method moveText (line 8480) | moveText(x, y) {
    method setLeadingMoveText (line 8484) | setLeadingMoveText(x, y) {
    method setTextMatrix (line 8488) | setTextMatrix(a, b, c, d, e, f) {
    method nextLine (line 8494) | nextLine() {
    method #getScaledPath (line 8497) | #getScaledPath(path, currentTransform, transform) {
    method paintChar (line 8502) | paintChar(character, x, y, patternFillTransform, patternStrokeTransfor...
    method isFontSubpixelAAEnabled (line 8570) | get isFontSubpixelAAEnabled() {
    method showText (line 8586) | showText(glyphs) {
    method showType3Text (line 8723) | showType3Text(glyphs) {
    method setCharWidth (line 8774) | setCharWidth(xWidth, yWidth) {}
    method setCharWidthAndBounds (line 8775) | setCharWidthAndBounds(xWidth, yWidth, llx, lly, urx, ury) {
    method getColorN_Pattern (line 8780) | getColorN_Pattern(IR) {
    method setStrokeColorN (line 8796) | setStrokeColorN() {
    method setFillColorN (line 8800) | setFillColorN() {
    method setStrokeRGBColor (line 8804) | setStrokeRGBColor(r, g, b) {
    method setStrokeTransparent (line 8808) | setStrokeTransparent() {
    method setFillRGBColor (line 8812) | setFillRGBColor(r, g, b) {
    method setFillTransparent (line 8816) | setFillTransparent() {
    method _getPattern (line 8820) | _getPattern(objId, matrix = null) {
    method shadingFill (line 8833) | shadingFill(objId) {
    method beginInlineImage (line 8855) | beginInlineImage() {
    method beginImageData (line 8858) | beginImageData() {
    method paintFormXObjectBegin (line 8861) | paintFormXObjectBegin(matrix, bbox) {
    method paintFormXObjectEnd (line 8880) | paintFormXObjectEnd() {
    method beginGroup (line 8887) | beginGroup(group) {
    method endGroup (line 8957) | endGroup(group) {
    method beginAnnotation (line 8981) | beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) {
    method endAnnotation (line 9029) | endAnnotation() {
    method paintImageMaskXObject (line 9038) | paintImageMaskXObject(img) {
    method paintImageMaskXObjectRepeat (line 9064) | paintImageMaskXObjectRepeat(img, scaleX, skewX = 0, skewY = 0, scaleY,...
    method paintImageMaskXObjectGroup (line 9083) | paintImageMaskXObjectGroup(images) {
    method paintImageXObject (line 9114) | paintImageXObject(objId) {
    method paintImageXObjectRepeat (line 9125) | paintImageXObjectRepeat(objId, scaleX, scaleY, positions) {
    method applyTransferMapsToCanvas (line 9148) | applyTransferMapsToCanvas(ctx) {
    method applyTransferMapsToBitmap (line 9156) | applyTransferMapsToBitmap(imgData) {
    method paintInlineImageXObject (line 9172) | paintInlineImageXObject(imgData) {
    method paintInlineImageXObjectGroup (line 9206) | paintInlineImageXObjectGroup(imgData, map) {
    method paintSolidColorImageMask (line 9231) | paintSolidColorImageMask() {
    method markPoint (line 9238) | markPoint(tag) {}
    method markPointProps (line 9239) | markPointProps(tag, properties) {}
    method beginMarkedContent (line 9240) | beginMarkedContent(tag) {
    method beginMarkedContentProps (line 9245) | beginMarkedContentProps(tag, properties) {
    method endMarkedContent (line 9257) | endMarkedContent() {
    method beginCompat (line 9261) | beginCompat() {}
    method endCompat (line 9262) | endCompat() {}
    method consumePath (line 9263) | consumePath(path, clipBox) {
    method getSinglePixelWidth (line 9285) | getSinglePixelWidth() {
    method getScaleForStroking (line 9299) | getScaleForStroking() {
    method rescaleAndStroke (line 9348) | rescaleAndStroke(path, saveRestore) {
    method isContentVisible (line 9381) | isContentVisible() {
  class GlobalWorkerOptions (line 9397) | class GlobalWorkerOptions {
    method workerPort (line 9400) | static get workerPort() {
    method workerPort (line 9403) | static set workerPort(val) {
    method workerSrc (line 9409) | static get workerSrc() {
    method workerSrc (line 9412) | static set workerSrc(val) {
  class Metadata (line 9422) | class Metadata {
    method constructor (line 9425) | constructor({
    method getRaw (line 9432) | getRaw() {
    method get (line 9435) | get(name) {
    method getAll (line 9438) | getAll() {
    method has (line 9441) | has(name) {
  constant INTERNAL (line 9449) | const INTERNAL = Symbol("INTERNAL");
  class OptionalContentGroup (line 9450) | class OptionalContentGroup {
    method constructor (line 9455) | constructor(renderingIntent, {
    method visible (line 9468) | get visible() {
    method _setVisible (line 9486) | _setVisible(internal, visible, userSet = false) {
  class OptionalContentConfig (line 9494) | class OptionalContentConfig {
    method constructor (line 9499) | constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) {
    method #evaluateVisibilityExpression (line 9525) | #evaluateVisibilityExpression(array) {
    method isVisible (line 9561) | isVisible(group) {
    method setVisibility (line 9630) | setVisibility(id, visible = true, preserveRB = true) {
    method setOCGState (line 9648) | setOCGState({
    method hasInitialVisibility (line 9679) | get hasInitialVisibility() {
    method getOrder (line 9682) | getOrder() {
    method getGroups (line 9691) | getGroups() {
    method getGroup (line 9694) | getGroup(id) {
    method getHash (line 9697) | getHash() {
  class PDFDataTransportStream (line 9712) | class PDFDataTransportStream {
    method constructor (line 9713) | constructor(pdfDataRangeTransport, {
    method _onReceiveData (line 9759) | _onReceiveData({
    method _progressiveDataLength (line 9781) | get _progressiveDataLength() {
    method _onProgress (line 9784) | _onProgress(evt) {
    method _onProgressiveDone (line 9796) | _onProgressiveDone() {
    method _removeRangeReader (line 9800) | _removeRangeReader(reader) {
    method getFullReader (line 9806) | getFullReader() {
    method getRangeReader (line 9812) | getRangeReader(begin, end) {
    method cancelAllRequests (line 9821) | cancelAllRequests(reason) {
  class PDFDataTransportStreamReader (line 9829) | class PDFDataTransportStreamReader {
    method constructor (line 9830) | constructor(stream, queuedChunks, progressiveDone = false, contentDisp...
    method _enqueue (line 9844) | _enqueue(chunk) {
    method headersReady (line 9859) | get headersReady() {
    method filename (line 9862) | get filename() {
    method isRangeSupported (line 9865) | get isRangeSupported() {
    method isStreamingSupported (line 9868) | get isStreamingSupported() {
    method contentLength (line 9871) | get contentLength() {
    method read (line 9874) | async read() {
    method cancel (line 9892) | cancel(reason) {
    method progressiveDone (line 9902) | progressiveDone() {
  class PDFDataTransportStreamRangeReader (line 9909) | class PDFDataTransportStreamRangeReader {
    method constructor (line 9910) | constructor(stream, begin, end) {
    method _enqueue (line 9919) | _enqueue(chunk) {
    method isStreamingSupported (line 9942) | get isStreamingSupported() {
    method read (line 9945) | async read() {
    method cancel (line 9964) | cancel(reason) {
  function getFilenameFromContentDispositionHeader (line 9979) | function getFilenameFromContentDispositionHeader(contentDisposition) {
  function createHeaders (line 10112) | function createHeaders(isHttp, httpHeaders) {
  function getResponseOrigin (line 10125) | function getResponseOrigin(url) {
  function validateRangeRequestCapabilities (line 10128) | function validateRangeRequestCapabilities({
  function extractFilenameFromHeader (line 10159) | function extractFilenameFromHeader(responseHeaders) {
  function createResponseError (line 10174) | function createResponseError(status, url) {
  function validateResponseStatus (line 10177) | function validateResponseStatus(status) {
  function createFetchOptions (line 10184) | function createFetchOptions(headers, withCredentials, abortController) {
  function getArrayBuffer (line 10194) | function getArrayBuffer(val) {
  class PDFFetchStream (line 10204) | class PDFFetchStream {
    method constructor (line 10206) | constructor(source) {
    method _progressiveDataLength (line 10213) | get _progressiveDataLength() {
    method getFullReader (line 10216) | getFullReader() {
    method getRangeReader (line 10221) | getRangeReader(begin, end) {
    method cancelAllRequests (line 10229) | cancelAllRequests(reason) {
  class PDFFetchStreamReader (line 10236) | class PDFFetchStreamReader {
    method constructor (line 10237) | constructor(stream) {
    method headersReady (line 10282) | get headersReady() {
    method filename (line 10285) | get filename() {
    method contentLength (line 10288) | get contentLength() {
    method isRangeSupported (line 10291) | get isRangeSupported() {
    method isStreamingSupported (line 10294) | get isStreamingSupported() {
    method read (line 10297) | async read() {
    method cancel (line 10319) | cancel(reason) {
  class PDFFetchStreamRangeReader (line 10324) | class PDFFetchStreamRangeReader {
    method constructor (line 10325) | constructor(stream, begin, end) {
    method isStreamingSupported (line 10350) | get isStreamingSupported() {
    method read (line 10353) | async read() {
    method cancel (line 10374) | cancel(reason) {
  constant OK_RESPONSE (line 10383) | const OK_RESPONSE = 200;
  constant PARTIAL_CONTENT_RESPONSE (line 10384) | const PARTIAL_CONTENT_RESPONSE = 206;
  function network_getArrayBuffer (line 10385) | function network_getArrayBuffer(xhr) {
  class NetworkManager (line 10392) | class NetworkManager {
    method constructor (line 10394) | constructor({
    method request (line 10406) | request(args) {
    method onProgress (line 10437) | onProgress(xhrId, evt) {
    method onStateChange (line 10444) | onStateChange(xhrId, evt) {
    method getRequestXhr (line 10493) | getRequestXhr(xhrId) {
    method isPendingRequest (line 10496) | isPendingRequest(xhrId) {
    method abortRequest (line 10499) | abortRequest(xhrId) {
  class PDFNetworkStream (line 10505) | class PDFNetworkStream {
    method constructor (line 10506) | constructor(source) {
    method _onRangeRequestReaderClosed (line 10513) | _onRangeRequestReaderClosed(reader) {
    method getFullReader (line 10519) | getFullReader() {
    method getRangeReader (line 10524) | getRangeReader(begin, end) {
    method cancelAllRequests (line 10530) | cancelAllRequests(reason) {
  class PDFNetworkStreamFullRequestReader (line 10537) | class PDFNetworkStreamFullRequestReader {
    method constructor (line 10538) | constructor(manager, source) {
    method _onHeadersReceived (line 10563) | _onHeadersReceived() {
    method _onDone (line 10591) | _onDone(data) {
    method _onError (line 10615) | _onError(status) {
    method _onProgress (line 10624) | _onProgress(evt) {
    method filename (line 10630) | get filename() {
    method isRangeSupported (line 10633) | get isRangeSupported() {
    method isStreamingSupported (line 10636) | get isStreamingSupported() {
    method contentLength (line 10639) | get contentLength() {
    method headersReady (line 10642) | get headersReady() {
    method read (line 10645) | async read() {
    method cancel (line 10667) | cancel(reason) {
  class PDFNetworkStreamRangeRequestReader (line 10683) | class PDFNetworkStreamRangeRequestReader {
    method constructor (line 10684) | constructor(manager, begin, end) {
    method _onHeadersReceived (line 10702) | _onHeadersReceived() {
    method _close (line 10709) | _close() {
    method _onDone (line 10712) | _onDone(data) {
    method _onError (line 10733) | _onError(status) {
    method _onProgress (line 10741) | _onProgress(evt) {
    method isStreamingSupported (line 10748) | get isStreamingSupported() {
    method read (line 10751) | async read() {
    method cancel (line 10773) | cancel(reason) {
  function parseUrlOrPath (line 10793) | function parseUrlOrPath(sourceUrl) {
  class PDFNodeStream (line 10800) | class PDFNodeStream {
    method constructor (line 10801) | constructor(source) {
    method _progressiveDataLength (line 10808) | get _progressiveDataLength() {
    method getFullReader (line 10811) | getFullReader() {
    method getRangeReader (line 10816) | getRangeReader(start, end) {
    method cancelAllRequests (line 10824) | cancelAllRequests(reason) {
  class PDFNodeStreamFsFullReader (line 10831) | class PDFNodeStreamFsFullReader {
    method constructor (line 10832) | constructor(stream) {
    method headersReady (line 10864) | get headersReady() {
    method filename (line 10867) | get filename() {
    method contentLength (line 10870) | get contentLength() {
    method isRangeSupported (line 10873) | get isRangeSupported() {
    method isStreamingSupported (line 10876) | get isStreamingSupported() {
    method read (line 10879) | async read() {
    method cancel (line 10906) | cancel(reason) {
    method _error (line 10913) | _error(reason) {
    method _setReadableStream (line 10917) | _setReadableStream(readableStream) {
  class PDFNodeStreamFsRangeReader (line 10938) | class PDFNodeStreamFsRangeReader {
    method constructor (line 10939) | constructor(stream, start, end) {
    method isStreamingSupported (line 10955) | get isStreamingSupported() {
    method read (line 10958) | async read() {
    method cancel (line 10984) | cancel(reason) {
    method _error (line 10991) | _error(reason) {
    method _setReadableStream (line 10995) | _setReadableStream(readableStream) {
  constant MAX_TEXT_DIVS_TO_RENDER (line 11017) | const MAX_TEXT_DIVS_TO_RENDER = 100000;
  constant DEFAULT_FONT_SIZE (line 11018) | const DEFAULT_FONT_SIZE = 30;
  class TextLayer (line 11019) | class TextLayer {
    method constructor (line 11043) | constructor({
    method fontFamilyMap (line 11085) | static get fontFamilyMap() {
    method render (line 11092) | render() {
    method update (line 11113) | update({
    method cancel (line 11141) | cancel() {
    method textDivs (line 11147) | get textDivs() {
    method textContentItemsStr (line 11150) | get textContentItemsStr() {
    method #processItems (line 11153) | #processItems(items) {
    method #appendText (line 11184) | #appendText(geom) {
    method #layout (line 11259) | #layout(params) {
    method cleanup (line 11295) | static cleanup() {
    method #getCtx (line 11307) | static #getCtx(lang = null) {
    method #ensureCtxFont (line 11326) | static #ensureCtxFont(ctx, size, family) {
    method #ensureMinFontSizeComputed (line 11335) | static #ensureMinFontSizeComputed() {
    method #getAscent (line 11349) | static #getAscent(fontFamily, style, lang) {
  class XfaText (line 11380) | class XfaText {
    method textContent (line 11381) | static textContent(xfa) {
    method shouldBuildText (line 11417) | static shouldBuildText(name) {
  constant DEFAULT_RANGE_CHUNK_SIZE (line 11444) | const DEFAULT_RANGE_CHUNK_SIZE = 65536;
  constant RENDERING_CANCELLED_TIMEOUT (line 11445) | const RENDERING_CANCELLED_TIMEOUT = 100;
  function getDocument (line 11446) | function getDocument(src = {}) {
  function getUrlProp (line 11622) | function getUrlProp(val) {
  function getDataProp (line 11637) | function getDataProp(val) {
  function getFactoryUrlProp (line 11652) | function getFactoryUrlProp(val) {
  class PDFDocumentLoadingTask (line 11664) | class PDFDocumentLoadingTask {
    method promise (line 11673) | get promise() {
    method destroy (line 11676) | async destroy() {
    method getData (line 11693) | async getData() {
  class PDFDataRangeTransport (line 11697) | class PDFDataRangeTransport {
    method constructor (line 11698) | constructor(length, initialData, progressiveDone = false, contentDispo...
    method addRangeListener (line 11709) | addRangeListener(listener) {
    method addProgressListener (line 11712) | addProgressListener(listener) {
    method addProgressiveReadListener (line 11715) | addProgressiveReadListener(listener) {
    method addProgressiveDoneListener (line 11718) | addProgressiveDoneListener(listener) {
    method onDataRange (line 11721) | onDataRange(begin, chunk) {
    method onDataProgress (line 11726) | onDataProgress(loaded, total) {
    method onDataProgressiveRead (line 11733) | onDataProgressiveRead(chunk) {
    method onDataProgressiveDone (line 11740) | onDataProgressiveDone() {
    method transportReady (line 11747) | transportReady() {
    method requestDataRange (line 11750) | requestDataRange(begin, end) {
    method abort (line 11753) | abort() {}
  class PDFDocumentProxy (line 11755) | class PDFDocumentProxy {
    method constructor (line 11756) | constructor(pdfInfo, transport) {
    method annotationStorage (line 11760) | get annotationStorage() {
    method canvasFactory (line 11763) | get canvasFactory() {
    method filterFactory (line 11766) | get filterFactory() {
    method numPages (line 11769) | get numPages() {
    method fingerprints (line 11772) | get fingerprints() {
    method isPureXfa (line 11775) | get isPureXfa() {
    method allXfaHtml (line 11778) | get allXfaHtml() {
    method getPage (line 11781) | getPage(pageNumber) {
    method getPageIndex (line 11784) | getPageIndex(ref) {
    method getDestinations (line 11787) | getDestinations() {
    method getDestination (line 11790) | getDestination(id) {
    method getPageLabels (line 11793) | getPageLabels() {
    method getPageLayout (line 11796) | getPageLayout() {
    method getPageMode (line 11799) | getPageMode() {
    method getViewerPreferences (line 11802) | getViewerPreferences() {
    method getOpenAction (line 11805) | getOpenAction() {
    method getAttachments (line 11808) | getAttachments() {
    method getJSActions (line 11811) | getJSActions() {
    method getOutline (line 11814) | getOutline() {
    method getOptionalContentConfig (line 11817) | getOptionalContentConfig({
    method getPermissions (line 11825) | getPermissions() {
    method getMetadata (line 11828) | getMetadata() {
    method getMarkInfo (line 11831) | getMarkInfo() {
    method getData (line 11834) | getData() {
    method saveDocument (line 11837) | saveDocument() {
    method getDownloadInfo (line 11840) | getDownloadInfo() {
    method cleanup (line 11843) | cleanup(keepLoadedFonts = false) {
    method destroy (line 11846) | destroy() {
    method cachedPageNumber (line 11849) | cachedPageNumber(ref) {
    method loadingParams (line 11852) | get loadingParams() {
    method loadingTask (line 11855) | get loadingTask() {
    method getFieldObjects (line 11858) | getFieldObjects() {
    method hasJSActions (line 11861) | hasJSActions() {
    method getCalculationOrderIds (line 11864) | getCalculationOrderIds() {
  class PDFPageProxy (line 11868) | class PDFPageProxy {
    method constructor (line 11870) | constructor(pageIndex, pageInfo, transport, pdfBug = false) {
    method pageNumber (line 11881) | get pageNumber() {
    method rotate (line 11884) | get rotate() {
    method ref (line 11887) | get ref() {
    method userUnit (line 11890) | get userUnit() {
    method view (line 11893) | get view() {
    method getViewport (line 11896) | getViewport({
    method getAnnotations (line 11913) | getAnnotations({
    method getJSActions (line 11921) | getJSActions() {
    method filterFactory (line 11924) | get filterFactory() {
    method isPureXfa (line 11927) | get isPureXfa() {
    method getXfa (line 11930) | async getXfa() {
    method render (line 11933) | render({
    method getOperatorList (line 12036) | getOperatorList({
    method streamTextContent (line 12071) | streamTextContent({
    method getTextContent (line 12087) | getTextContent(params = {}) {
    method getStructTree (line 12117) | getStructTree() {
    method _destroy (line 12120) | _destroy() {
    method cleanup (line 12141) | cleanup(resetStats = false) {
    method #tryCleanup (line 12149) | #tryCleanup() {
    method _startRenderPage (line 12166) | _startRenderPage(transparency, cacheKey) {
    method _renderPageChunk (line 12174) | _renderPageChunk(operatorListChunk, intentState) {
    method _pumpOperatorList (line 12188) | _pumpOperatorList({
    method _abortOperatorList (line 12245) | _abortOperatorList({
    method stats (line 12290) | get stats() {
  class LoopbackPort (line 12294) | class LoopbackPort {
    method postMessage (line 12297) | postMessage(obj, transfer) {
    method addEventListener (line 12309) | addEventListener(name, listener, options = null) {
    method removeEventListener (line 12325) | removeEventListener(name, listener) {
    method terminate (line 12330) | terminate() {
  class PDFWorker (line 12337) | class PDFWorker {
    method constructor (line 12361) | constructor({
    method promise (line 12383) | get promise() {
    method #resolve (line 12386) | #resolve() {
    method port (line 12392) | get port() {
    method messageHandler (line 12395) | get messageHandler() {
    method _initializeFromPort (line 12398) | _initializeFromPort(port) {
    method _initialize (line 12404) | _initialize() {
    method _setupFakeWorker (line 12472) | _setupFakeWorker() {
    method destroy (line 12493) | destroy() {
    method fromPort (line 12502) | static fromPort(params) {
    method workerSrc (line 12515) | static get workerSrc() {
    method #mainThreadWorkerMessageHandler (line 12521) | static get #mainThreadWorkerMessageHandler() {
    method _setupFakeWorkerGlobal (line 12528) | static get _setupFakeWorkerGlobal() {
  class WorkerTransport (line 12542) | class WorkerTransport {
    method constructor (line 12548) | constructor(messageHandler, loadingTask, networkStream, params, factor...
    method #cacheSimpleMethod (line 12571) | #cacheSimpleMethod(name, data = null) {
    method annotationStorage (line 12580) | get annotationStorage() {
    method getRenderingIntent (line 12583) | getRenderingIntent(intent, annotationMode = AnnotationMode.ENABLE, pri...
    method destroy (line 12633) | destroy() {
    method setupMessageHandler (line 12665) | setupMessageHandler() {
    method getData (line 12901) | getData() {
    method saveDocument (line 12904) | saveDocument() {
    method getPage (line 12921) | getPage(pageNumber) {
    method getPageIndex (line 12946) | getPageIndex(ref) {
    method getAnnotations (line 12955) | getAnnotations(pageIndex, intent) {
    method getFieldObjects (line 12961) | getFieldObjects() {
    method hasJSActions (line 12964) | hasJSActions() {
    method getCalculationOrderIds (line 12967) | getCalculationOrderIds() {
    method getDestinations (line 12970) | getDestinations() {
    method getDestination (line 12973) | getDestination(id) {
    method getPageLabels (line 12981) | getPageLabels() {
    method getPageLayout (line 12984) | getPageLayout() {
    method getPageMode (line 12987) | getPageMode() {
    method getViewerPreferences (line 12990) | getViewerPreferences() {
    method getOpenAction (line 12993) | getOpenAction() {
    method getAttachments (line 12996) | getAttachments() {
    method getDocJSActions (line 12999) | getDocJSActions() {
    method getPageJSActions (line 13002) | getPageJSActions(pageIndex) {
    method getStructTree (line 13007) | getStructTree(pageIndex) {
    method getOutline (line 13012) | getOutline() {
    method getOptionalContentConfig (line 13015) | getOptionalContentConfig(renderingIntent) {
    method getPermissions (line 13018) | getPermissions() {
    method getMetadata (line 13021) | getMetadata() {
    method getMarkInfo (line 13036) | getMarkInfo() {
    method startCleanup (line 13039) | async startCleanup(keepLoadedFonts = false) {
    method cachedPageNumber (line 13058) | cachedPageNumber(ref) {
  constant INITIAL_DATA (line 13066) | const INITIAL_DATA = Symbol("INITIAL_DATA");
  class PDFObjects (line 13067) | class PDFObjects {
    method #ensureObj (line 13069) | #ensureObj(objId) {
    method get (line 13075) | get(objId, callback = null) {
    method has (line 13087) | has(objId) {
    method delete (line 13091) | delete(objId) {
    method resolve (line 13099) | resolve(objId, data = null) {
    method clear (line 13104) | clear() {
  method [Symbol.iterator] (line 13113) | *[Symbol.iterator]() {
  class RenderTask (line 13125) | class RenderTask {
    method constructor (line 13129) | constructor(internalRenderTask) {
    method promise (line 13132) | get promise() {
    method cancel (line 13135) | cancel(extraDelay = 0) {
    method separateAnnots (line 13138) | get separateAnnots() {
  class InternalRenderTask (line 13151) | class InternalRenderTask {
    method constructor (line 13154) | constructor({
    method completed (line 13193) | get completed() {
    method initializeGraphics (line 13196) | initializeGraphics({
    method cancel (line 13233) | cancel(error = null, extraDelay = 0) {
    method operatorListChanged (line 13246) | operatorListChanged() {
    method _continue (line 13257) | _continue() {
    method _scheduleNext (line 13268) | _scheduleNext() {
    method _next (line 13278) | async _next() {
  function makeColorComp (line 13297) | function makeColorComp(n) {
  function scaleAndClamp (line 13300) | function scaleAndClamp(x) {
  class ColorConverters (line 13303) | class ColorConverters {
    method CMYK_G (line 13304) | static CMYK_G([c, y, m, k]) {
    method G_CMYK (line 13307) | static G_CMYK([g]) {
    method G_RGB (line 13310) | static G_RGB([g]) {
    method G_rgb (line 13313) | static G_rgb([g]) {
    method G_HTML (line 13317) | static G_HTML([g]) {
    method RGB_G (line 13321) | static RGB_G([r, g, b]) {
    method RGB_rgb (line 13324) | static RGB_rgb(color) {
    method RGB_HTML (line 13327) | static RGB_HTML(color) {
    method T_HTML (line 13330) | static T_HTML() {
    method T_rgb (line 13333) | static T_rgb() {
    method CMYK_RGB (line 13336) | static CMYK_RGB([c, y, m, k]) {
    method CMYK_rgb (line 13339) | static CMYK_rgb([c, y, m, k]) {
    method CMYK_HTML (line 13342) | static CMYK_HTML(components) {
    method RGB_CMYK (line 13346) | static RGB_CMYK([r, g, b]) {
  class BaseSVGFactory (line 13358) | class BaseSVGFactory {
    method create (line 13359) | create(width, height, skipDimensions = false) {
    method createElement (line 13373) | createElement(type) {
    method _createSVG (line 13379) | _createSVG(type) {
  class DOMSVGFactory (line 13383) | class DOMSVGFactory extends BaseSVGFactory {
    method _createSVG (line 13384) | _createSVG(type) {
  class XfaLayer (line 13391) | class XfaLayer {
    method setupStorage (line 13392) | static setupStorage(html, id, element, storage, intent) {
    method setAttributes (line 13460) | static setAttributes({
    method render (line 13508) | static render(parameters) {
    method update (line 13593) | static update(parameters) {
  constant DEFAULT_TAB_INDEX (line 13607) | const DEFAULT_TAB_INDEX = 1000;
  class AnnotationElementFactory (line 13610) | class AnnotationElementFactory {
    method create (line 13611) | static create(parameters) {
  class AnnotationElement (line 13671) | class AnnotationElement {
    method constructor (line 13675) | constructor(parameters, {
    method _hasPopupData (line 13700) | static _hasPopupData({
    method _isEditable (line 13707) | get _isEditable() {
    method hasPopupData (line 13710) | get hasPopupData() {
    method updateEdited (line 13713) | updateEdited(params) {
    method resetEdited (line 13728) | resetEdited() {
    method #setRectEdited (line 13736) | #setRectEdited(rect) {
    method _createContainer (line 13766) | _createContainer(ignoreBorder) {
    method setRotation (line 13860) | setRotation(angle, container = this.container) {
    method _commonActions (line 13879) | get _commonActions() {
    method _dispatchEventFromSandbox (line 13957) | _dispatchEventFromSandbox(actions, jsEvent) {
    method _setDefaultPropertiesFromJS (line 13964) | _setDefaultPropertiesFromJS(element) {
    method _createQuadrilaterals (line 13987) | _createQuadrilaterals() {
    method _createPopup (line 14057) | _createPopup() {
    method render (line 14078) | render() {
    method _getElementsByName (line 14081) | _getElementsByName(name, skipId = null) {
    method show (line 14131) | show() {
    method hide (line 14137) | hide() {
    method getElementsToTriggerPopup (line 14143) | getElementsToTriggerPopup() {
    method addHighlightArea (line 14146) | addHighlightArea() {
    method _editOnDoubleClick (line 14156) | _editOnDoubleClick() {
    method width (line 14174) | get width() {
    method height (line 14177) | get height() {
  class LinkAnnotationElement (line 14181) | class LinkAnnotationElement extends AnnotationElement {
    method constructor (line 14182) | constructor(parameters, options = null) {
    method render (line 14190) | render() {
    method #setInternalLink (line 14232) | #setInternalLink() {
    method _bindLink (line 14235) | _bindLink(link, destination) {
    method _bindNamedAction (line 14247) | _bindNamedAction(link, action) {
    method #bindAttachment (line 14255) | #bindAttachment(link, attachment, dest = null) {
    method #bindSetOCGState (line 14266) | #bindSetOCGState(link, action) {
    method _bindJSAction (line 14274) | _bindJSAction(link, data) {
    method _bindResetFormAction (line 14298) | _bindResetFormAction(link, resetForm) {
  class TextAnnotationElement (line 14401) | class TextAnnotationElement extends AnnotationElement {
    method constructor (line 14402) | constructor(parameters) {
    method render (line 14407) | render() {
  class WidgetAnnotationElement (line 14422) | class WidgetAnnotationElement extends AnnotationElement {
    method render (line 14423) | render() {
    method showElementAndHideCanvas (line 14426) | showElementAndHideCanvas(element) {
    method _getKeyModifier (line 14434) | _getKeyModifier(event) {
    method _setEventListener (line 14437) | _setEventListener(element, elementData, baseName, eventName, valueGett...
    method _setEventListeners (line 14478) | _setEventListeners(element, elementData, names, getter) {
    method _setBackgroundColor (line 14495) | _setBackgroundColor(element) {
    method _setTextStyle (line 14499) | _setTextStyle(element) {
    method _setRequired (line 14524) | _setRequired(element, isRequired) {
  class TextWidgetAnnotationElement (line 14533) | class TextWidgetAnnotationElement extends WidgetAnnotationElement {
    method constructor (line 14534) | constructor(parameters) {
    method setPropertyOnSiblings (line 14540) | setPropertyOnSiblings(base, key, value, keyInStorage) {
    method render (line 14551) | render() {
  class SignatureWidgetAnnotationElement (line 14851) | class SignatureWidgetAnnotationElement extends WidgetAnnotationElement {
    method constructor (line 14852) | constructor(parameters) {
  class CheckboxWidgetAnnotationElement (line 14858) | class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
    method constructor (line 14859) | constructor(parameters) {
    method render (line 14864) | render() {
  class RadioButtonWidgetAnnotationElement (line 14932) | class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
    method constructor (line 14933) | constructor(parameters) {
    method render (line 14938) | render() {
  class PushButtonWidgetAnnotationElement (line 15015) | class PushButtonWidgetAnnotationElement extends LinkAnnotationElement {
    method constructor (line 15016) | constructor(parameters) {
    method render (line 15021) | render() {
  class ChoiceWidgetAnnotationElement (line 15034) | class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
    method constructor (line 15035) | constructor(parameters) {
    method render (line 15040) | render() {
  class PopupAnnotationElement (line 15256) | class PopupAnnotationElement extends AnnotationElement {
    method constructor (line 15257) | constructor(parameters) {
    method render (line 15268) | render() {
  class PopupElement (line 15294) | class PopupElement {
    method constructor (line 15314) | constructor({
    method render (line 15352) | render() {
    method #html (line 15394) | get #html() {
    method #fontSize (line 15402) | get #fontSize() {
    method #fontColor (line 15405) | get #fontColor() {
    method #makePopupContent (line 15408) | #makePopupContent(text) {
    method _formatContents (line 15438) | _formatContents({
    method #keyDown (line 15455) | #keyDown(event) {
    method updateEdited (line 15463) | updateEdited({
    method resetEdited (line 15481) | resetEdited() {
    method #setPosition (line 15494) | #setPosition() {
    method #toggle (line 15532) | #toggle() {
    method #show (line 15544) | #show() {
    method #hide (line 15556) | #hide() {
    method forceHide (line 15564) | forceHide() {
    method maybeShow (line 15571) | maybeShow() {
    method isVisible (line 15581) | get isVisible() {
  class FreeTextAnnotationElement (line 15585) | class FreeTextAnnotationElement extends AnnotationElement {
    method constructor (line 15586) | constructor(parameters) {
    method render (line 15595) | render() {
  class LineAnnotationElement (line 15615) | class LineAnnotationElement extends AnnotationElement {
    method constructor (line 15617) | constructor(parameters) {
    method render (line 15623) | render() {
    method getElementsToTriggerPopup (line 15646) | getElementsToTriggerPopup() {
    method addHighlightArea (line 15649) | addHighlightArea() {
  class SquareAnnotationElement (line 15653) | class SquareAnnotationElement extends AnnotationElement {
    method constructor (line 15655) | constructor(parameters) {
    method render (line 15661) | render() {
    method getElementsToTriggerPopup (line 15685) | getElementsToTriggerPopup() {
    method addHighlightArea (line 15688) | addHighlightArea() {
  class CircleAnnotationElement (line 15692) | class CircleAnnotationElement extends AnnotationElement {
    method constructor (line 15694) | constructor(parameters) {
    method render (line 15700) | render() {
    method getElementsToTriggerPopup (line 15724) | getElementsToTriggerPopup() {
    method addHighlightArea (line 15727) | addHighlightArea() {
  class PolylineAnnotationElement (line 15731) | class PolylineAnnotationElement extends AnnotationElement {
    method constructor (line 15733) | constructor(parameters) {
    method render (line 15741) | render() {
    method getElementsToTriggerPopup (line 15776) | getElementsToTriggerPopup() {
    method addHighlightArea (line 15779) | addHighlightArea() {
  class PolygonAnnotationElement (line 15783) | class PolygonAnnotationElement extends PolylineAnnotationElement {
    method constructor (line 15784) | constructor(parameters) {
  class CaretAnnotationElement (line 15790) | class CaretAnnotationElement extends AnnotationElement {
    method constructor (line 15791) | constructor(parameters) {
    method render (line 15797) | render() {
  class InkAnnotationElement (line 15805) | class InkAnnotationElement extends AnnotationElement {
    method constructor (line 15808) | constructor(parameters) {
    method #getTransform (line 15817) | #getTransform(rotation, rect) {
    method render (line 15845) | render() {
    method updateEdited (line 15884) | updateEdited(params) {
    method getElementsToTriggerPopup (line 15911) | getElementsToTriggerPopup() {
    method addHighlightArea (line 15914) | addHighlightArea() {
  class HighlightAnnotationElement (line 15918) | class HighlightAnnotationElement extends AnnotationElement {
    method constructor (line 15919) | constructor(parameters) {
    method render (line 15927) | render() {
  class UnderlineAnnotationElement (line 15936) | class UnderlineAnnotationElement extends AnnotationElement {
    method constructor (line 15937) | constructor(parameters) {
    method render (line 15944) | render() {
  class SquigglyAnnotationElement (line 15952) | class SquigglyAnnotationElement extends AnnotationElement {
    method constructor (line 15953) | constructor(parameters) {
    method render (line 15960) | render() {
  class StrikeOutAnnotationElement (line 1
Copy disabled (too large) Download .json
Condensed preview — 1489 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (24,804K chars).
[
  {
    "path": ".coveragerc",
    "chars": 132,
    "preview": "[run]\nomit =\n  */apps.py,\n  */migrations/*,\n  */settings*,\n  */test*,\n  */tests/*,\n  *urls.py,\n  */wsgi*,\n  manage.py,\n "
  },
  {
    "path": ".devcontainer/Dockerfile",
    "chars": 1077,
    "preview": "FROM python:3.13-alpine3.22\n\n#Install all dependencies.\nRUN apk add --no-cache postgresql-libs postgresql-client gettext"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 898,
    "preview": "// For format details, see https://aka.ms/devcontainer.json.\n{\n\t\"name\": \"Tandoor Dev Container\",\n\t\"build\": { \"context\": "
  },
  {
    "path": ".dockerignore",
    "chars": 395,
    "preview": "**/node_modules\nnpm-debug.log\nDockerfile*\ndocker-compose*\n.dockerignore\n.gitignore\nREADME.md\nLICENSE\n.vscode\n.env\n.env.t"
  },
  {
    "path": ".flake8",
    "chars": 506,
    "preview": "[flake8]\nextend-ignore =\n    # Whitespace before ':' - Required for black compatibility\n    E203,\n    # Line break occur"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 646,
    "preview": "# These are supported funding model platforms\n\ngithub: [vabene1111]\npatreon: # Replace with a single Patreon username\nop"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md.bak",
    "chars": 1992,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Version\n<!-- "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1745,
    "preview": "name: Bug Report\ndescription: \"Create a report to help us improve\"\n#title: \"\"\n#labels: [\"Bug\"]\nbody:\n  - type: markdown\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 168,
    "preview": "blank_issues_enabled: true\ncontact_links:\n  - name: FAQs\n    url: https://docs.tandoor.dev/faq/\n    about: Please take a"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/doc_issue.yml",
    "chars": 1135,
    "preview": "name: Documentation Issue\ndescription: \"Create a report to help us improve\"\n#title: \"\"\nlabels: [\"documentation\"]\nbody:\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md.bak",
    "chars": 604,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1091,
    "preview": "name: Feature Request\ndescription: \"Suggest an idea for this project\"\n#title: \"\"\nlabels: [\"enhancement\"]\nbody:\n  - type:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/help-request.md.bak",
    "chars": 742,
    "preview": "---\nname: Help request\nabout: If there is anything wrong with your setup\ntitle: ''\nlabels: setup issue\nassignees: ''\n\n--"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/help_request.yml",
    "chars": 2326,
    "preview": "name: Help request\ndescription: \"If there is anything wrong with your setup\"\n#title: \"\"\nlabels: [\"setup issue\"]\nbody:\n  "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/url_import.md.bak",
    "chars": 451,
    "preview": "---\nname: Website Import\nabout: Anything related to website imports\ntitle: ''\nlabels: enhancement, url_import\nassignees:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/website_import.yml",
    "chars": 1166,
    "preview": "name: Website Import\ndescription: \"Anything related to website imports\"\n#title: \"\"\n#labels: [\"enhancement\"]\nbody:\n  - ty"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 747,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/build-docker.yml",
    "chars": 4053,
    "preview": "name: Build Docker Container\n\non: push\n\njobs:\n  build-container:\n    name: Build ${{ matrix.name }} Container\n    runs-o"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 3111,
    "preview": "name: Continuous Integration\n\non: [push, pull_request]\n\njobs:\n    build:\n        if: github.repository_owner == 'Tandoor"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 1698,
    "preview": "name: \"Code scanning - action\"\n\non:\n  push:\n  pull_request:\n  schedule:\n    - cron: '0 13 * * 2'\n\njobs:\n  CodeQL-Build:\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 545,
    "preview": "name: Make Docs\non:\n  # the 1st condition\n  workflow_run:\n    workflows: [\"Continuous Integration\"]\n    branches: [maste"
  },
  {
    "path": ".gitignore",
    "chars": 1374,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 149,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"Defaul"
  },
  {
    "path": ".idea/dictionaries/vaben.xml",
    "chars": 211,
    "preview": "<component name=\"ProjectDictionaryState\">\n  <dictionary name=\"vaben\">\n    <words>\n      <w>mealplan</w>\n      <w>pinia</"
  },
  {
    "path": ".idea/dictionaries/vabene1111_PC.xml",
    "chars": 348,
    "preview": "<component name=\"ProjectDictionaryState\">\n  <dictionary name=\"vabene1111-PC\">\n    <words>\n      <w>autosync</w>\n      <w"
  },
  {
    "path": ".idea/inspectionProfiles/profiles_settings.xml",
    "chars": 228,
    "preview": "<component name=\"InspectionProjectProfileManager\">\n  <settings>\n    <option name=\"PROJECT_PROFILE\" value=\"Default\" />\n  "
  },
  {
    "path": ".idea/modules.xml",
    "chars": 266,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n   "
  },
  {
    "path": ".idea/prettier.xml",
    "chars": 188,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"PrettierConfiguration\">\n    <option name"
  },
  {
    "path": ".idea/recipes.iml",
    "chars": 1418,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n  <component name=\"FacetManager\">\n    <"
  },
  {
    "path": ".idea/watcherTasks.xml",
    "chars": 3963,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectTasksOptions\">\n    <TaskOptions i"
  },
  {
    "path": ".prettierignore",
    "chars": 207,
    "preview": "# generated files\napi.ts\nvue/src/apps/*.js\nvue/node_modules\nstaticfiles/\ndocs/reports/\n/vue3/src/openapi/\n\n# ignored fil"
  },
  {
    "path": ".prettierrc",
    "chars": 127,
    "preview": "{\n    \"printWidth\": 179,\n    \"trailingComma\": \"es5\",\n    \"tabWidth\": 2,\n    \"semi\": false,\n    \"experimentalTernaries\": "
  },
  {
    "path": ".vscode/launch.json",
    "chars": 911,
    "preview": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 365,
    "preview": "{\n    \"python.testing.pytestArgs\": [\n        \"cookbook/tests\"\n    ],\n    \"python.testing.unittestEnabled\": false,\n    \"p"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 2033,
    "preview": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"Run Migrations\",\n      \"type\": \"shell\",\n      \"command\": \"pyt"
  },
  {
    "path": "CONTRIBUTERS.md",
    "chars": 2669,
    "preview": "# Contributers\n\nMany thanks to everyone who contributed to this project! If you add something or help out feel free to a"
  },
  {
    "path": "Dockerfile",
    "chars": 2188,
    "preview": "FROM python:3.13-alpine3.23\n\n#Install all dependencies.\nRUN apk add --no-cache postgresql-libs postgresql-client gettext"
  },
  {
    "path": "LICENSE.md",
    "chars": 36734,
    "preview": "“Commons Clause” License Condition v1.0\n\nThe Software is provided to you by the Licensor under the License, as defined b"
  },
  {
    "path": "README.md",
    "chars": 7469,
    "preview": "<h1 align=\"center\">\n  <br>\n  <a href=\"https://tandoor.dev\"><img src=\"https://github.com/vabene1111/recipes/raw/develop/d"
  },
  {
    "path": "SECURITY.md",
    "chars": 316,
    "preview": "# Security Policy\n\n## Supported Versions\n\nSince this software is still considered beta/WIP support is always only given "
  },
  {
    "path": "boot.sh",
    "chars": 3806,
    "preview": "#!/bin/sh\nsource venv/bin/activate\n\n# these are envsubst in the nginx config, make sure they default to something sensib"
  },
  {
    "path": "cookbook/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cookbook/admin.py",
    "chars": 13025,
    "preview": "from django.conf import settings\nfrom django.contrib import admin\nfrom django.contrib.auth.admin import UserAdmin\nfrom d"
  },
  {
    "path": "cookbook/apps.py",
    "chars": 1971,
    "preview": "import traceback\n\nfrom django.apps import AppConfig\nfrom django.conf import settings\nfrom django.db import OperationalEr"
  },
  {
    "path": "cookbook/connectors/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "cookbook/connectors/connector.py",
    "chars": 1984,
    "preview": "from abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom typing import Optional\n\nfrom cookbook.models "
  },
  {
    "path": "cookbook/connectors/connector_manager.py",
    "chars": 8992,
    "preview": "import asyncio\nimport logging\nimport queue\nimport threading\nfrom asyncio import Task\nfrom dataclasses import dataclass\nf"
  },
  {
    "path": "cookbook/connectors/homeassistant.py",
    "chars": 3905,
    "preview": "import logging\nfrom logging import Logger\nfrom typing import Dict, Tuple\nfrom urllib.parse import urljoin\n\nfrom aiohttp "
  },
  {
    "path": "cookbook/fixtures/german_example_tags.json",
    "chars": 5223,
    "preview": "[\n  {\n    \"model\": \"cookbook.keyword\",\n    \"pk\": 1,\n    \"fields\": {\n      \"name\": \"Hauptgang\",\n      \"icon\": \"\\ud83c\\udf"
  },
  {
    "path": "cookbook/forms.py",
    "chars": 7358,
    "preview": "from datetime import datetime\n\nfrom allauth.account.forms import ResetPasswordForm, SignupForm\nfrom allauth.socialaccoun"
  },
  {
    "path": "cookbook/helper/AllAuthCustomAdapter.py",
    "chars": 2049,
    "preview": "\nimport datetime\nimport logging\n\nfrom gettext import gettext as _\n\nfrom allauth.account.adapter import DefaultAccountAda"
  },
  {
    "path": "cookbook/helper/CustomStorageClass.py",
    "chars": 563,
    "preview": "import hashlib\n\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom storages.backends.s3boto3 impo"
  },
  {
    "path": "cookbook/helper/HelperFunctions.py",
    "chars": 2035,
    "preview": "import socket\nimport requests\nimport struct\nfrom ipaddress import ip_address\nfrom urllib.parse import urlparse, quote, u"
  },
  {
    "path": "cookbook/helper/__init__.py",
    "chars": 85,
    "preview": "from cookbook.helper.AllAuthCustomAdapter import AllAuthCustomAdapter\n\n__all__ = [\n]\n"
  },
  {
    "path": "cookbook/helper/ai_helper.py",
    "chars": 2887,
    "preview": "from decimal import Decimal\n\nfrom django.utils import timezone\nfrom django.db.models import Sum\nfrom litellm import Cust"
  },
  {
    "path": "cookbook/helper/automation_helper.py",
    "chars": 11400,
    "preview": "import re\n\nfrom django.core.cache import caches\nfrom django.db.models.functions import Lower\n\nfrom cookbook.models impor"
  },
  {
    "path": "cookbook/helper/batch_edit_helper.py",
    "chars": 1181,
    "preview": "def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):\n    \"\"\"\n    given a mod"
  },
  {
    "path": "cookbook/helper/cache_helper.py",
    "chars": 305,
    "preview": "class CacheHelper:\n    space = None\n\n    BASE_UNITS_CACHE_KEY = None\n    PROPERTY_TYPE_CACHE_KEY = None\n\n    def __init_"
  },
  {
    "path": "cookbook/helper/context_processors.py",
    "chars": 626,
    "preview": "from django.conf import settings\n\n\ndef context_settings(request):\n    return {\n        'EMAIL_ENABLED': settings.EMAIL_H"
  },
  {
    "path": "cookbook/helper/cooklang_parser.py",
    "chars": 11314,
    "preview": "# Cooklang parser forked from on https://github.com/luizribeiro/py-cooklang cooklang.py as of 11/18/25 - MIT License\n# M"
  },
  {
    "path": "cookbook/helper/drf_spectacular_hooks.py",
    "chars": 4071,
    "preview": "# custom processing for schema\n# reason: DRF writable nested needs ID's to decide if a nested object should be created o"
  },
  {
    "path": "cookbook/helper/fdc_helper.py",
    "chars": 712,
    "preview": "import json\n\n\ndef get_all_nutrient_types():\n    f = open('')  # <--- download the foundation food or any other dataset a"
  },
  {
    "path": "cookbook/helper/image_processing.py",
    "chars": 2959,
    "preview": "import os\nfrom io import BytesIO\n\nfrom PIL import Image\n\n\ndef rescale_image_jpeg(image_object, base_width=1020):\n    img"
  },
  {
    "path": "cookbook/helper/ingredient_parser.py",
    "chars": 14107,
    "preview": "import re\nimport string\nimport unicodedata\nfrom django.db.models import Q\n\nfrom cookbook.helper.automation_helper import"
  },
  {
    "path": "cookbook/helper/mdx_attributes.py",
    "chars": 852,
    "preview": "import markdown\nfrom markdown.treeprocessors import Treeprocessor\n\n\nclass StyleTreeprocessor(Treeprocessor):\n\n    def ru"
  },
  {
    "path": "cookbook/helper/mdx_urlize.py",
    "chars": 2259,
    "preview": "\"\"\"\nA more liberal autolinker\n\nInspired by Django's urlize function.\n\nPositive examples:\n\n>>> import markdown\n>>> md = m"
  },
  {
    "path": "cookbook/helper/open_data_importer.py",
    "chars": 23411,
    "preview": "import traceback\nfrom collections import defaultdict\nfrom decimal import Decimal\n\nfrom cookbook.models import (Food, Foo"
  },
  {
    "path": "cookbook/helper/permission_config.py",
    "chars": 187,
    "preview": "from cookbook.helper.permission_helper import CustomIsUser\n\n\nclass PermissionConfig:\n    BOOKS = {\n        'owner': True"
  },
  {
    "path": "cookbook/helper/permission_helper.py",
    "chars": 21524,
    "preview": "import inspect\n\nfrom django.conf import settings\nfrom django.contrib import messages\nfrom django.contrib.auth.decorators"
  },
  {
    "path": "cookbook/helper/property_helper.py",
    "chars": 5637,
    "preview": "from django.core.cache import caches\n\nfrom cookbook.helper.cache_helper import CacheHelper\nfrom cookbook.helper.unit_con"
  },
  {
    "path": "cookbook/helper/recipe_search.py",
    "chars": 28797,
    "preview": "import json\nfrom datetime import date, timedelta\n\nfrom django.contrib.postgres.search import SearchQuery, SearchRank, Se"
  },
  {
    "path": "cookbook/helper/recipe_url_import.py",
    "chars": 18733,
    "preview": "import re\nimport traceback\nfrom html import unescape\n\nfrom django.utils.dateparse import parse_duration\nfrom django.util"
  },
  {
    "path": "cookbook/helper/scope_middleware.py",
    "chars": 4305,
    "preview": "import re\n\nfrom django.http import HttpResponseRedirect\nfrom django.urls import reverse\nfrom django_scopes import scope,"
  },
  {
    "path": "cookbook/helper/shopping_helper.py",
    "chars": 10135,
    "preview": "\nfrom decimal import Decimal\n\nfrom django.db.models import F, OuterRef, Q, Subquery, Value\nfrom django.db.models.functio"
  },
  {
    "path": "cookbook/helper/template_helper.py",
    "chars": 3953,
    "preview": "from gettext import gettext as _\n\nimport bleach\nimport markdown as md\nfrom jinja2 import Template, TemplateSyntaxError, "
  },
  {
    "path": "cookbook/helper/unit_conversion_helper.py",
    "chars": 7112,
    "preview": "from django.core.cache import caches\nfrom decimal import Decimal\n\nfrom cookbook.helper.cache_helper import CacheHelper\nf"
  },
  {
    "path": "cookbook/integration/cheftap.py",
    "chars": 2487,
    "preview": "import re\n\nfrom cookbook.helper.ingredient_parser import IngredientParser\nfrom cookbook.integration.integration import I"
  },
  {
    "path": "cookbook/integration/chowdown.py",
    "chars": 10737,
    "preview": "import re\nimport unicodedata\nfrom io import BytesIO, StringIO\nfrom zipfile import ZipFile\n\nfrom cookbook.helper.image_pr"
  },
  {
    "path": "cookbook/integration/cookbookapp.py",
    "chars": 5460,
    "preview": "import re\nfrom io import BytesIO\nfrom typing import Any\n\nfrom cookbook.helper.HelperFunctions import safe_request\nimport"
  },
  {
    "path": "cookbook/integration/cooklang.py",
    "chars": 7656,
    "preview": "import os\nfrom io import BytesIO, StringIO\n\nfrom recipe_scrapers._utils import get_minutes\n\nfrom cookbook.helper.cooklan"
  },
  {
    "path": "cookbook/integration/cookmate.py",
    "chars": 3827,
    "preview": "from io import BytesIO\n\nfrom cookbook.helper.HelperFunctions import safe_request\nfrom cookbook.helper.ingredient_parser "
  },
  {
    "path": "cookbook/integration/copymethat.py",
    "chars": 5844,
    "preview": "from io import BytesIO\nfrom zipfile import ZipFile\n\nfrom bs4 import BeautifulSoup, Tag\nfrom django.utils.translation imp"
  },
  {
    "path": "cookbook/integration/default.py",
    "chars": 3517,
    "preview": "import json\nimport traceback\nfrom io import BytesIO, StringIO\nfrom re import match\nfrom zipfile import ZipFile\n\nfrom res"
  },
  {
    "path": "cookbook/integration/domestica.py",
    "chars": 2042,
    "preview": "import base64\nimport json\nfrom io import BytesIO\n\nfrom cookbook.helper.ingredient_parser import IngredientParser\nfrom co"
  },
  {
    "path": "cookbook/integration/gourmet.py",
    "chars": 8593,
    "preview": "import base64\nfrom io import BytesIO\nfrom lxml import etree\nimport requests\nfrom pathlib import Path\n\nfrom bs4 import Be"
  },
  {
    "path": "cookbook/integration/integration.py",
    "chars": 15620,
    "preview": "import traceback\nimport uuid\nfrom io import BytesIO\nfrom zipfile import BadZipFile, ZipFile\n\nfrom bs4 import Tag\nfrom dj"
  },
  {
    "path": "cookbook/integration/mealie.py",
    "chars": 4513,
    "preview": "import json\nimport re\nfrom io import BytesIO\nfrom zipfile import ZipFile\n\nfrom cookbook.helper.image_processing import g"
  },
  {
    "path": "cookbook/integration/mealie1.py",
    "chars": 20428,
    "preview": "import json\nimport re\nimport traceback\nimport uuid\nfrom decimal import Decimal\nfrom io import BytesIO\nfrom zipfile impor"
  },
  {
    "path": "cookbook/integration/mealmaster.py",
    "chars": 3404,
    "preview": "import re\n\nfrom cookbook.helper.ingredient_parser import IngredientParser\nfrom cookbook.integration.integration import I"
  },
  {
    "path": "cookbook/integration/melarecipes.py",
    "chars": 3213,
    "preview": "import base64\nimport json\nfrom io import BytesIO\n\nfrom gettext import gettext as _\nfrom cookbook.helper.ingredient_parse"
  },
  {
    "path": "cookbook/integration/nextcloud_cookbook.py",
    "chars": 8038,
    "preview": "import json\nimport re\nfrom io import BytesIO, StringIO\nfrom zipfile import ZipFile\n\nfrom PIL import Image\n\nfrom cookbook"
  },
  {
    "path": "cookbook/integration/openeats.py",
    "chars": 5923,
    "preview": "import json\n\nfrom django.utils.translation import gettext as _\n\nfrom cookbook.helper.ingredient_parser import Ingredient"
  },
  {
    "path": "cookbook/integration/paprika.py",
    "chars": 4953,
    "preview": "import base64\nimport gzip\nimport json\nimport re\nfrom gettext import gettext as _\nfrom io import BytesIO\n\nfrom cookbook.h"
  },
  {
    "path": "cookbook/integration/pdfexport.py",
    "chars": 1878,
    "preview": "import asyncio\n\nimport django.core.management.commands.runserver as runserver\nfrom asgiref.sync import sync_to_async\nfro"
  },
  {
    "path": "cookbook/integration/pepperplate.py",
    "chars": 2412,
    "preview": "from cookbook.helper.ingredient_parser import IngredientParser\nfrom cookbook.integration.integration import Integration\n"
  },
  {
    "path": "cookbook/integration/plantoeat.py",
    "chars": 4342,
    "preview": "from io import BytesIO\n\nfrom cookbook.helper.HelperFunctions import safe_request\nfrom cookbook.helper.ingredient_parser "
  },
  {
    "path": "cookbook/integration/recettetek.py",
    "chars": 5816,
    "preview": "import json\nimport re\nfrom io import BytesIO\nfrom zipfile import ZipFile\n\nfrom django.utils.translation import gettext a"
  },
  {
    "path": "cookbook/integration/recipekeeper.py",
    "chars": 4001,
    "preview": "import re\nfrom io import BytesIO\nfrom zipfile import ZipFile\n\nfrom bs4 import BeautifulSoup\n\nfrom django.utils.translati"
  },
  {
    "path": "cookbook/integration/recipesage.py",
    "chars": 5649,
    "preview": "import json\nimport html\nfrom io import BytesIO\n\nfrom cookbook.helper.HelperFunctions import safe_request\nfrom cookbook.h"
  },
  {
    "path": "cookbook/integration/rezeptsuitede.py",
    "chars": 3375,
    "preview": "import base64\nfrom io import BytesIO\nfrom lxml import etree\n\nfrom cookbook.helper.ingredient_parser import IngredientPar"
  },
  {
    "path": "cookbook/integration/rezkonv.py",
    "chars": 3511,
    "preview": "from cookbook.helper.ingredient_parser import IngredientParser\nfrom cookbook.integration.integration import Integration\n"
  },
  {
    "path": "cookbook/integration/saffron.py",
    "chars": 3991,
    "preview": "from django.utils.translation import gettext as _\n\nfrom cookbook.helper.ingredient_parser import IngredientParser\nfrom c"
  },
  {
    "path": "cookbook/locale/ar/LC_MESSAGES/django.po",
    "chars": 57274,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/bg/LC_MESSAGES/django.po",
    "chars": 102865,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/ca/LC_MESSAGES/django.po",
    "chars": 112727,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/cs/LC_MESSAGES/django.po",
    "chars": 85354,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/da/LC_MESSAGES/django.po",
    "chars": 100170,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/de/LC_MESSAGES/django.po",
    "chars": 119176,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/el/LC_MESSAGES/django.po",
    "chars": 73791,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/en/LC_MESSAGES/django.po",
    "chars": 50715,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/es/LC_MESSAGES/django.po",
    "chars": 100666,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/fi/LC_MESSAGES/django.po",
    "chars": 61694,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/fr/LC_MESSAGES/django.po",
    "chars": 119085,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/he/LC_MESSAGES/django.po",
    "chars": 55075,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/hr/LC_MESSAGES/django.po",
    "chars": 66456,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/hu_HU/LC_MESSAGES/django.po",
    "chars": 112231,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/hy/LC_MESSAGES/django.po",
    "chars": 86441,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/id/LC_MESSAGES/django.po",
    "chars": 58455,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/it/LC_MESSAGES/django.po",
    "chars": 119373,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/ko/LC_MESSAGES/django.po",
    "chars": 64408,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/lv/LC_MESSAGES/django.po",
    "chars": 91405,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/nb_NO/LC_MESSAGES/django.po",
    "chars": 77421,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/nl/LC_MESSAGES/django.po",
    "chars": 121506,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/nn/LC_MESSAGES/django.po",
    "chars": 50829,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/pl/LC_MESSAGES/django.po",
    "chars": 91873,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/pt/LC_MESSAGES/django.po",
    "chars": 71993,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/pt_BR/LC_MESSAGES/django.po",
    "chars": 99135,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/rn/LC_MESSAGES/django.po",
    "chars": 50723,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/ro/LC_MESSAGES/django.po",
    "chars": 100051,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/ru/LC_MESSAGES/django.po",
    "chars": 68120,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/sl/LC_MESSAGES/django.po",
    "chars": 92549,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/sv/LC_MESSAGES/django.po",
    "chars": 91235,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/tr/LC_MESSAGES/django.po",
    "chars": 58456,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/tr/id/LC_MESSAGES/django.po",
    "chars": 69159,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/uk/LC_MESSAGES/django.po",
    "chars": 92247,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/vi/LC_MESSAGES/django.po",
    "chars": 65607,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/zh_CN/LC_MESSAGES/django.po",
    "chars": 85593,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/locale/zh_Hant/LC_MESSAGES/django.po",
    "chars": 75440,
    "preview": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same "
  },
  {
    "path": "cookbook/management/commands/export.py",
    "chars": 280,
    "preview": "from django.core.management.commands.dumpdata import Command as DumpdataCommand\nfrom django_scopes import scopes_disable"
  },
  {
    "path": "cookbook/management/commands/fix_duplicate_properties.py",
    "chars": 1738,
    "preview": "from django.conf import settings\nfrom django.contrib.postgres.search import SearchVector\nfrom django.core.management.bas"
  },
  {
    "path": "cookbook/management/commands/import.py",
    "chars": 280,
    "preview": "from django.core.management.commands.loaddata import Command as LoaddataCommand\nfrom django_scopes import scopes_disable"
  },
  {
    "path": "cookbook/management/commands/rebuildindex.py",
    "chars": 1493,
    "preview": "from django.conf import settings\nfrom django.contrib.postgres.search import SearchVector\nfrom django.core.management.bas"
  },
  {
    "path": "cookbook/management/commands/seed_basic_data.py",
    "chars": 832,
    "preview": "from django.conf import settings\nfrom django.contrib.auth.models import User\nfrom django.contrib.postgres.search import "
  },
  {
    "path": "cookbook/managers.py",
    "chars": 1612,
    "preview": "from django.contrib.postgres.aggregates import StringAgg\nfrom django.contrib.postgres.search import SearchQuery, SearchR"
  },
  {
    "path": "cookbook/migrations/0001_initial.py",
    "chars": 6045,
    "preview": "# Generated by Django 2.2.7 on 2019-11-19 18:43\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0001_squashed_0227_space_ai_default_provider_and_more.py",
    "chars": 79447,
    "preview": "# Generated by Django 5.2.6 on 2025-09-10 18:59\n\nimport annoying.fields\nfrom django_scopes import scopes_disabled\n\nimpor"
  },
  {
    "path": "cookbook/migrations/0002_auto_20191119_2035.py",
    "chars": 366,
    "preview": "# Generated by Django 2.2.7 on 2019-11-19 19:35\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0003_enable_pgtrm.py",
    "chars": 268,
    "preview": "from django.contrib.postgres.operations import TrigramExtension\nfrom django.db import migrations\n\n\nclass Migration(migra"
  },
  {
    "path": "cookbook/migrations/0004_storage_created_by.py",
    "chars": 622,
    "preview": "# Generated by Django 3.0 on 2019-12-09 10:30\n\nfrom django.conf import settings\nfrom django.db import migrations, models"
  },
  {
    "path": "cookbook/migrations/0005_recipebook_recipebookentry.py",
    "chars": 1231,
    "preview": "# Generated by Django 2.2.9 on 2019-12-24 11:14\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0006_recipe_image.py",
    "chars": 423,
    "preview": "# Generated by Django 3.0.1 on 2019-12-25 15:11\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0007_auto_20191226_0852.py",
    "chars": 526,
    "preview": "# Generated by Django 3.0.1 on 2019-12-26 07:52\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0008_mealplan.py",
    "chars": 1106,
    "preview": "# Generated by Django 3.0.2 on 2020-01-17 14:55\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0009_auto_20200130_1056.py",
    "chars": 849,
    "preview": "# Generated by Django 3.0.2 on 2020-01-30 09:56\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0010_auto_20200130_1059.py",
    "chars": 813,
    "preview": "# Generated by Django 3.0.2 on 2020-01-30 09:59\n\nfrom django.db import migrations\nfrom django_scopes import scopes_disab"
  },
  {
    "path": "cookbook/migrations/0011_remove_recipeingredients_unit.py",
    "chars": 338,
    "preview": "# Generated by Django 3.0.2 on 2020-01-30 10:16\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0012_auto_20200130_1116.py",
    "chars": 386,
    "preview": "# Generated by Django 3.0.2 on 2020-01-30 10:16\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0013_userpreference.py",
    "chars": 879,
    "preview": "# Generated by Django 3.0.2 on 2020-02-13 22:15\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0014_auto_20200213_2332.py",
    "chars": 887,
    "preview": "# Generated by Django 3.0.2 on 2020-02-13 22:32\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0015_auto_20200213_2334.py",
    "chars": 589,
    "preview": "# Generated by Django 3.0.2 on 2020-02-13 22:34\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0016_auto_20200213_2335.py",
    "chars": 731,
    "preview": "# Generated by Django 3.0.2 on 2020-02-13 22:35\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0017_auto_20200216_2257.py",
    "chars": 523,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 21:57\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0018_auto_20200216_2303.py",
    "chars": 352,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:03\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0019_ingredient.py",
    "chars": 532,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:03\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0020_recipeingredient_ingredient.py",
    "chars": 492,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:08\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0021_auto_20200216_2309.py",
    "chars": 850,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:09\nfrom django.db import migrations\nfrom django_scopes import scopes_disabl"
  },
  {
    "path": "cookbook/migrations/0022_remove_recipeingredient_name.py",
    "chars": 337,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:11\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0023_auto_20200216_2311.py",
    "chars": 386,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:11\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0024_auto_20200216_2313.py",
    "chars": 376,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 22:13\n\nfrom django.db import migrations\n\n\nclass Migration(migrations.Migration"
  },
  {
    "path": "cookbook/migrations/0025_userpreference_nav_color.py",
    "chars": 604,
    "preview": "# Generated by Django 3.0.2 on 2020-02-16 23:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0026_auto_20200219_1605.py",
    "chars": 594,
    "preview": "# Generated by Django 3.0.2 on 2020-02-19 15:05\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0027_ingredient_recipe.py",
    "chars": 499,
    "preview": "# Generated by Django 3.0.4 on 2020-03-17 17:31\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0028_auto_20200317_1901.py",
    "chars": 490,
    "preview": "# Generated by Django 3.0.4 on 2020-03-17 18:01\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0029_auto_20200317_1901.py",
    "chars": 479,
    "preview": "# Generated by Django 3.0.4 on 2020-03-17 18:01\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0030_recipeingredient_note.py",
    "chars": 416,
    "preview": "# Generated by Django 3.0.4 on 2020-03-17 18:02\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0031_auto_20200407_1841.py",
    "chars": 412,
    "preview": "# Generated by Django 3.0.4 on 2020-04-07 16:41\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0032_userpreference_default_unit.py",
    "chars": 412,
    "preview": "# Generated by Django 3.0.4 on 2020-04-13 20:34\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0033_userpreference_default_page.py",
    "chars": 481,
    "preview": "# Generated by Django 3.0.4 on 2020-04-13 20:41\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0034_auto_20200426_1614.py",
    "chars": 590,
    "preview": "# Generated by Django 3.0.5 on 2020-04-26 14:14\n\nfrom django.db import migrations\nfrom django_scopes import scopes_disab"
  },
  {
    "path": "cookbook/migrations/0035_auto_20200427_1637.py",
    "chars": 780,
    "preview": "# Generated by Django 3.0.5 on 2020-04-27 14:37\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0036_auto_20200427_1800.py",
    "chars": 640,
    "preview": "# Generated by Django 3.0.5 on 2020-04-27 16:00\n\nfrom django.db import migrations\nfrom django_scopes import scopes_disab"
  },
  {
    "path": "cookbook/migrations/0037_userpreference_search_style.py",
    "chars": 466,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 10:45\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0038_auto_20200502_1259.py",
    "chars": 576,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 10:59\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0039_recipebook_shared.py",
    "chars": 549,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 12:04\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0040_auto_20200502_1433.py",
    "chars": 651,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 12:33\n\nimport annoying.fields\nfrom django.conf import settings\nfrom django.db "
  },
  {
    "path": "cookbook/migrations/0041_auto_20200502_1446.py",
    "chars": 674,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 12:46\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0042_cooklog.py",
    "chars": 1023,
    "preview": "# Generated by Django 3.0.5 on 2020-05-02 14:47\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0043_auto_20200507_2302.py",
    "chars": 765,
    "preview": "# Generated by Django 3.0.5 on 2020-05-07 21:02\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0044_viewlog.py",
    "chars": 901,
    "preview": "# Generated by Django 3.0.5 on 2020-05-11 10:21\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0045_userpreference_show_recent.py",
    "chars": 389,
    "preview": "# Generated by Django 3.0.5 on 2020-06-02 08:51\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0046_auto_20200602_1133.py",
    "chars": 839,
    "preview": "# Generated by Django 3.0.5 on 2020-06-02 09:33\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0047_auto_20200602_1133.py",
    "chars": 1330,
    "preview": "# Generated by Django 3.0.5 on 2020-06-02 09:33\n\nfrom django.db import migrations\nfrom django.utils.translation import g"
  },
  {
    "path": "cookbook/migrations/0048_auto_20200602_1140.py",
    "chars": 583,
    "preview": "# Generated by Django 3.0.5 on 2020-06-02 09:40\n\nfrom django.db import migrations, models\nimport django.db.models.deleti"
  },
  {
    "path": "cookbook/migrations/0049_mealtype_created_by.py",
    "chars": 595,
    "preview": "# Generated by Django 3.0.7 on 2020-06-11 13:08\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0050_auto_20200611_1509.py",
    "chars": 994,
    "preview": "# Generated by Django 3.0.7 on 2020-06-11 13:09\n\nfrom django.db import migrations\nfrom django.db.models import Q\nfrom dj"
  },
  {
    "path": "cookbook/migrations/0051_auto_20200611_1518.py",
    "chars": 586,
    "preview": "# Generated by Django 3.0.7 on 2020-06-11 13:18\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0052_userpreference_ingredient_decimals.py",
    "chars": 405,
    "preview": "# Generated by Django 3.0.7 on 2020-06-11 20:14\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0053_auto_20200611_2217.py",
    "chars": 446,
    "preview": "# Generated by Django 3.0.7 on 2020-06-11 20:17\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "cookbook/migrations/0054_sharelink.py",
    "chars": 1018,
    "preview": "# Generated by Django 3.0.7 on 2020-06-16 08:57\n\nfrom django.conf import settings\nfrom django.db import migrations, mode"
  },
  {
    "path": "cookbook/migrations/0055_auto_20200616_1236.py",
    "chars": 598,
    "preview": "# Generated by Django 3.0.7 on 2020-06-16 10:36\n\nfrom django.db import migrations, models\nimport uuid\n\n\nclass Migration("
  }
]

// ... and 1289 more files (download for full content)

About this extraction

This page contains the full source code of the TandoorRecipes/recipes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1489 files (22.5 MB), approximately 6.0M tokens, and a symbol index with 10776 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!