mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856f417d1b | ||
|
|
85821bcc94 | ||
|
|
2345af8fd6 | ||
|
|
51107c64ee | ||
|
|
81983c5ae2 | ||
|
|
f7713a43a7 | ||
|
|
ffd951a7f4 | ||
|
|
319ac8e191 | ||
|
|
aea247b4a3 | ||
|
|
e2843bb02f | ||
|
|
e3aa3e1137 | ||
|
|
da1187b03a | ||
|
|
f9ed79978c | ||
|
|
920a3ed4a3 | ||
|
|
2077eae142 | ||
|
|
b1ef35e415 | ||
|
|
0a687d840c | ||
|
|
6a3034b966 | ||
|
|
3d7afbfe4f | ||
|
|
02e43730bd | ||
|
|
6adf077ee5 | ||
|
|
d73ffa46ff | ||
|
|
8572f338ad | ||
|
|
920ec8e74b | ||
|
|
2328bf2342 | ||
|
|
85620a1431 |
16
boot.sh
16
boot.sh
@@ -2,7 +2,7 @@
|
||||
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:-8080}"
|
||||
export TANDOOR_PORT="${TANDOOR_PORT:-80}"
|
||||
export MEDIA_ROOT=${MEDIA_ROOT:-/opt/recipes/mediafiles};
|
||||
export STATIC_ROOT=${STATIC_ROOT:-/opt/recipes/staticfiles};
|
||||
|
||||
@@ -12,11 +12,6 @@ GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
|
||||
|
||||
PLUGINS_BUILD="${PLUGINS_BUILD:-0}"
|
||||
|
||||
if [ "${TANDOOR_PORT}" -eq 80 ]; then
|
||||
echo "TANDOOR_PORT set to 8080 because 80 is now taken by the integrated nginx"
|
||||
TANDOOR_PORT=8080
|
||||
fi
|
||||
|
||||
display_warning() {
|
||||
echo "[WARNING]"
|
||||
echo -e "$1"
|
||||
@@ -29,7 +24,6 @@ envsubst '$MEDIA_ROOT $STATIC_ROOT $TANDOOR_PORT' < /opt/recipes/http.d/Recipes.
|
||||
echo "Starting nginx"
|
||||
nginx
|
||||
|
||||
|
||||
echo "Checking configuration..."
|
||||
|
||||
# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
|
||||
@@ -110,9 +104,5 @@ chmod -R 755 ${MEDIA_ROOT:-/opt/recipes/mediafiles}
|
||||
ipv6_disable=$(cat /sys/module/ipv6/parameters/disable)
|
||||
|
||||
echo "Starting gunicorn"
|
||||
# Check if IPv6 is enabled, only then run gunicorn with ipv6 support
|
||||
if [ "$ipv6_disable" -eq 0 ]; then
|
||||
exec gunicorn -b "[::]:$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
else
|
||||
exec gunicorn -b ":$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
fi
|
||||
exec gunicorn --bind unix:/run/tandoor.sock --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ class RecipeSearch():
|
||||
def _favorite_recipes(self):
|
||||
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte:
|
||||
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
if less_than or self._timescooked == 0:
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
@@ -339,7 +339,7 @@ class RecipeSearch():
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
|
||||
if self._timescooked:
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
self._queryset = self._queryset.filter(favorite=self._timescooked)
|
||||
elif self._timescooked_lte:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
|
||||
elif self._timescooked_gte:
|
||||
|
||||
@@ -135,8 +135,9 @@ class UnitConversionHelper:
|
||||
:param food: base food
|
||||
:return: converted ingredient object from base amount/unit/food
|
||||
"""
|
||||
if uc.food is None or uc.food == food:
|
||||
if (uc.food is None or uc.food == food) and uc.converted_amount > 0 and uc.base_amount > 0:
|
||||
if unit == uc.base_unit:
|
||||
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
|
||||
else:
|
||||
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)
|
||||
return None
|
||||
|
||||
@@ -128,6 +128,7 @@ class Mealie1(Integration):
|
||||
|
||||
steps_relation = []
|
||||
first_step_of_recipe_dict = {}
|
||||
step_id_dict = {}
|
||||
for s in mealie_database['recipe_instructions']:
|
||||
if s['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
|
||||
@@ -135,9 +136,20 @@ class Mealie1(Integration):
|
||||
name=s['title'],
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
|
||||
step_id_dict[s["id"]] = step.pk
|
||||
if s['recipe_id'] not in first_step_of_recipe_dict:
|
||||
first_step_of_recipe_dict[s['recipe_id']] = step.pk
|
||||
|
||||
# it is possible for a recipe to not have steps but have ingredients, in that case create an empty step to add them to later
|
||||
for r in recipes_dict.keys():
|
||||
if r not in first_step_of_recipe_dict:
|
||||
step = Step.objects.create(instruction='',
|
||||
order=0,
|
||||
name='',
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[r], step_id=step.pk))
|
||||
first_step_of_recipe_dict[r] = step.pk
|
||||
|
||||
for n in mealie_database['notes']:
|
||||
if n['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=n['text'],
|
||||
@@ -153,6 +165,11 @@ class Mealie1(Integration):
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
|
||||
self.import_log.save()
|
||||
|
||||
# mealie stores the reference to a step (instruction) from an ingredient (reference) in the recipe_ingredient_ref_link table
|
||||
recipe_ingredient_ref_link_dict = {}
|
||||
for ref in mealie_database['recipe_ingredient_ref_link']:
|
||||
recipe_ingredient_ref_link_dict[ref["reference_id"]] = ref["instruction_id"]
|
||||
|
||||
ingredients_relation = []
|
||||
for i in mealie_database['recipes_ingredients']:
|
||||
if i['recipe_id'] in recipes_dict:
|
||||
@@ -162,18 +179,18 @@ class Mealie1(Integration):
|
||||
is_header=True,
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=title_ingredient.pk))
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=title_ingredient.pk))
|
||||
if i['food_id']:
|
||||
ingredient = Ingredient.objects.create(
|
||||
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
|
||||
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
|
||||
original_text=i['original_text'],
|
||||
order=i['position'],
|
||||
amount=i['quantity'],
|
||||
amount=i['quantity'] if i['quantity'] else 0,
|
||||
note=i['note'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
|
||||
elif i['note'].strip():
|
||||
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
@@ -186,7 +203,7 @@ class Mealie1(Integration):
|
||||
original_text=i['original_text'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
|
||||
Step.ingredients.through.objects.bulk_create(ingredients_relation)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
|
||||
@@ -340,3 +357,10 @@ class Mealie1(Integration):
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
|
||||
|
||||
def get_step_id(i, first_step_of_recipe_dict, step_id_dict, recipe_ingredient_ref_link_dict):
|
||||
try:
|
||||
return step_id_dict[recipe_ingredient_ref_link_dict[i['reference_id']]]
|
||||
except KeyError:
|
||||
return first_step_of_recipe_dict[i['recipe_id']]
|
||||
@@ -16,7 +16,7 @@ import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from cookbook.models import SearchFields
|
||||
|
||||
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
|
||||
|
||||
def allSearchFields():
|
||||
return list(SearchFields.objects.values_list('id', flat=True))
|
||||
@@ -141,6 +141,8 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
TrigramExtension(),
|
||||
UnaccentExtension(),
|
||||
migrations.RunPython(create_default_groups),
|
||||
migrations.CreateModel(
|
||||
name='AiProvider',
|
||||
|
||||
15
cookbook/migrations/0230_auto_20250925_2056.py
Normal file
15
cookbook/migrations/0230_auto_20250925_2056.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-25 18:56
|
||||
|
||||
from django.db import migrations
|
||||
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0229_alter_ailog_options_alter_aiprovider_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
TrigramExtension(),
|
||||
UnaccentExtension(),
|
||||
]
|
||||
@@ -0,0 +1,141 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-30 18:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0230_auto_20250925_2056'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='aiprovider',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='automation',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='bookmarkletimport',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='connectorconfig',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cooklog',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='customfilter',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='exportlog',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='food',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='importlog',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='invitelink',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='keyword',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mealplan',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mealtype',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='recipe',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='recipebook',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='recipeimport',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sharelink',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='shoppinglistentry',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='shoppinglistrecipe',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='space',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='storage',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='supermarket',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='supermarketcategory',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sync',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='synclog',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='telegrambot',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='unit',
|
||||
options={'ordering': ('name',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='unitconversion',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userfile',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userspace',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='viewlog',
|
||||
options={'ordering': ('pk',)},
|
||||
),
|
||||
]
|
||||
@@ -402,6 +402,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class AiProvider(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
@@ -421,13 +424,14 @@ class AiProvider(models.Model):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('id',)
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class AiLog(models.Model, PermissionModelMixin):
|
||||
F_FILE_IMPORT = 'FILE_IMPORT'
|
||||
F_STEP_SORT = 'STEP_SORT'
|
||||
F_FOOD_PROPERTIES = 'FOOD_PROPERTIES'
|
||||
F_RECIPE_PROPERTIES = 'RECIPE_PROPERTIES'
|
||||
|
||||
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
|
||||
function = models.CharField(max_length=64)
|
||||
@@ -476,6 +480,9 @@ class ConnectorConfig(models.Model, PermissionModelMixin):
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class UserPreference(models.Model, PermissionModelMixin):
|
||||
# Themes
|
||||
@@ -579,6 +586,9 @@ class UserSpace(models.Model, PermissionModelMixin):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class Storage(models.Model, PermissionModelMixin):
|
||||
DROPBOX = 'DB'
|
||||
@@ -603,6 +613,9 @@ class Storage(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class Sync(models.Model, PermissionModelMixin):
|
||||
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
|
||||
@@ -618,6 +631,9 @@ class Sync(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return self.path
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class SupermarketCategory(models.Model, PermissionModelMixin, MergeModelMixin):
|
||||
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||
@@ -643,6 +659,7 @@ class SupermarketCategory(models.Model, PermissionModelMixin, MergeModelMixin):
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space'),
|
||||
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_category_unique_open_data_slug_per_space')
|
||||
]
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class Supermarket(models.Model, PermissionModelMixin):
|
||||
@@ -662,6 +679,7 @@ class Supermarket(models.Model, PermissionModelMixin):
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space'),
|
||||
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_unique_open_data_slug_per_space')
|
||||
]
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
|
||||
@@ -693,6 +711,9 @@ class SyncLog(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return f"{self.created_at}:{self.sync} - {self.status}"
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelMixin):
|
||||
if SORT_TREE_BY_NAME:
|
||||
@@ -710,6 +731,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='kw_unique_name_per_space')
|
||||
]
|
||||
indexes = (Index(fields=['id', 'name']),)
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixin, MergeModelMixin):
|
||||
@@ -741,6 +763,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space'),
|
||||
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_unique_open_data_slug_per_space')
|
||||
]
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
@@ -874,6 +897,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
Index(fields=['id']),
|
||||
Index(fields=['name']),
|
||||
)
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model, PermissionModelMixin):
|
||||
@@ -900,6 +924,7 @@ class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model
|
||||
models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'], name='f_unique_conversion_per_space'),
|
||||
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_conversion_unique_open_data_slug_per_space')
|
||||
]
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
|
||||
@@ -1104,13 +1129,14 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
|
||||
sub_food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe__in=related_recipes).exclude(recipe=None).values_list('recipe'))
|
||||
return Recipe.objects.filter(Q(id__in=related_recipes.values_list('id')) | sub_step_recipes | sub_food_recipes)
|
||||
|
||||
class Meta():
|
||||
class Meta:
|
||||
indexes = (
|
||||
GinIndex(fields=["name_search_vector"]),
|
||||
GinIndex(fields=["desc_search_vector"]),
|
||||
Index(fields=['id']),
|
||||
Index(fields=['name']),
|
||||
)
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionModelMixin):
|
||||
@@ -1131,6 +1157,9 @@ class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionMod
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class RecipeImport(models.Model, PermissionModelMixin):
|
||||
@@ -1159,6 +1188,9 @@ class RecipeImport(models.Model, PermissionModelMixin):
|
||||
self.delete()
|
||||
return recipe
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=128)
|
||||
@@ -1176,6 +1208,7 @@ class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionMod
|
||||
|
||||
class Meta():
|
||||
indexes = (Index(fields=['name']),)
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, PermissionModelMixin):
|
||||
@@ -1221,6 +1254,7 @@ class MealType(models.Model, PermissionModelMixin):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['space', 'name', 'created_by'], name='mt_unique_name_per_space'),
|
||||
]
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
|
||||
@@ -1248,6 +1282,9 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
|
||||
def __str__(self):
|
||||
return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}'
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
|
||||
name = models.CharField(max_length=32, blank=True, default='')
|
||||
@@ -1263,6 +1300,9 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
|
||||
def __str__(self):
|
||||
return f'Shopping list recipe {self.id} - {self.recipe}'
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
|
||||
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
|
||||
@@ -1294,6 +1334,9 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
@@ -1309,6 +1352,9 @@ class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, Permissi
|
||||
def __str__(self):
|
||||
return f'{self.recipe} - {self.uuid}'
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
def default_valid_until():
|
||||
return date.today() + timedelta(days=14)
|
||||
@@ -1332,6 +1378,9 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
|
||||
def __str__(self):
|
||||
return f'{self.uuid}'
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class TelegramBot(models.Model, PermissionModelMixin):
|
||||
token = models.CharField(max_length=256)
|
||||
@@ -1346,6 +1395,9 @@ class TelegramBot(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
@@ -1363,7 +1415,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
|
||||
def __str__(self):
|
||||
return self.recipe.name
|
||||
|
||||
class Meta():
|
||||
class Meta:
|
||||
indexes = (
|
||||
Index(fields=['id']),
|
||||
Index(fields=['recipe']),
|
||||
@@ -1372,6 +1424,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
|
||||
Index(fields=['created_by']),
|
||||
Index(fields=['created_by', 'rating']),
|
||||
)
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
|
||||
@@ -1385,13 +1438,14 @@ class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionMo
|
||||
def __str__(self):
|
||||
return self.recipe.name
|
||||
|
||||
class Meta():
|
||||
class Meta:
|
||||
indexes = (
|
||||
Index(fields=['recipe']),
|
||||
Index(fields=['-created_at']),
|
||||
Index(fields=['created_by']),
|
||||
Index(fields=['recipe', '-created_at', 'created_by']),
|
||||
)
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ImportLog(models.Model, PermissionModelMixin):
|
||||
@@ -1412,6 +1466,9 @@ class ImportLog(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return f"{self.created_at}:{self.type}"
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class ExportLog(models.Model, PermissionModelMixin):
|
||||
type = models.CharField(max_length=32)
|
||||
@@ -1432,6 +1489,9 @@ class ExportLog(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return f"{self.created_at}:{self.type}"
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models.Model, PermissionModelMixin):
|
||||
html = models.TextField()
|
||||
@@ -1442,6 +1502,9 @@ class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models
|
||||
objects = ScopedManager(space='space')
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
# field names used to configure search behavior - all data populated during data migration
|
||||
# other option is to use a MultiSelectField from https://github.com/goinnn/django-multiselectfield
|
||||
@@ -1509,6 +1572,9 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
|
||||
def __str__(self):
|
||||
return f'{self.name} (#{self.id})'
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin):
|
||||
FOOD_ALIAS = 'FOOD_ALIAS'
|
||||
@@ -1555,6 +1621,9 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
||||
objects = ScopedManager(space='space')
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class CustomFilter(models.Model, PermissionModelMixin):
|
||||
RECIPE = 'RECIPE'
|
||||
@@ -1585,3 +1654,4 @@ class CustomFilter(models.Model, PermissionModelMixin):
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['space', 'name'], name='cf_unique_name_per_space')
|
||||
]
|
||||
ordering = ('pk',)
|
||||
|
||||
@@ -99,19 +99,19 @@ def test_list_filter(obj_1, u1_s1):
|
||||
|
||||
response = json.loads(
|
||||
u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?from_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}'
|
||||
f'{reverse(LIST_URL)}?from_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}'
|
||||
).content)['results']
|
||||
assert len(response) == 0
|
||||
|
||||
response = json.loads(
|
||||
u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?to_date={(timezone.now() - timedelta(days=2)).strftime("%Y-%m-%d")}'
|
||||
f'{reverse(LIST_URL)}?to_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}'
|
||||
).content)['results']
|
||||
assert len(response) == 0
|
||||
|
||||
response = json.loads(
|
||||
u1_s1.get(
|
||||
f'{reverse(LIST_URL)}?from_date={(timezone.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}'
|
||||
f'{reverse(LIST_URL)}?from_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}&to_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}'
|
||||
).content)['results']
|
||||
assert len(response) == 1
|
||||
|
||||
@@ -153,8 +153,8 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
|
||||
'id': meal_type.id,
|
||||
'name': meal_type.name
|
||||
},
|
||||
'from_date': (timezone.now()).strftime("%Y-%m-%d"),
|
||||
'to_date': (timezone.now()).strftime("%Y-%m-%d"),
|
||||
'from_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
|
||||
'to_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
|
||||
'servings': 1,
|
||||
'title': 'test',
|
||||
'shared': []
|
||||
@@ -196,8 +196,8 @@ def test_add_with_shopping(u1_s1, meal_type):
|
||||
'id': meal_type.id,
|
||||
'name': meal_type.name
|
||||
},
|
||||
'from_date': (timezone.now()).strftime("%Y-%m-%d"),
|
||||
'to_date': (timezone.now()).strftime("%Y-%m-%d"),
|
||||
'from_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
|
||||
'to_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
|
||||
'servings': 1,
|
||||
'title': 'test',
|
||||
'shared': [],
|
||||
@@ -212,13 +212,13 @@ def test_add_with_shopping(u1_s1, meal_type):
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['', 2],
|
||||
[f'?from_date={timezone.now().strftime("%Y-%m-%d")}', 1],
|
||||
[f'?from_date={timezone.localtime(timezone.now()).strftime("%Y-%m-%d")}', 1],
|
||||
[
|
||||
f'?to_date={(timezone.now() - timedelta(days=1)).strftime("%Y-%m-%d")}',
|
||||
f'?to_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}',
|
||||
1
|
||||
],
|
||||
[
|
||||
f'?from_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}',
|
||||
f'?from_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}&to_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}',
|
||||
0
|
||||
],
|
||||
])
|
||||
|
||||
@@ -185,3 +185,32 @@ def test_unit_conversions(space_1, space_2, u1_s1):
|
||||
assert next(x for x in conversions if x.unit == unit_kg_space_2) is not None
|
||||
assert abs(next(x for x in conversions if x.unit == unit_kg_space_2).amount - Decimal(0.1)) < 0.0001
|
||||
print(conversions)
|
||||
|
||||
def test_conversion_with_zero(space_1, space_2, u1_s1):
|
||||
with scopes_disabled():
|
||||
uch = UnitConversionHelper(space_1)
|
||||
|
||||
unit_gram = Unit.objects.create(name='gram', base_unit='g', space=space_1)
|
||||
unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit=None, space=space_1)
|
||||
|
||||
food_1 = Food.objects.create(name='Test Food 1', space=space_1)
|
||||
|
||||
ingredient_food_1_gram = Ingredient.objects.create(
|
||||
food=food_1,
|
||||
unit=unit_gram,
|
||||
amount=100,
|
||||
space=space_1,
|
||||
)
|
||||
|
||||
print('\n----------- TEST BASE CUSTOM CONVERSION - TO CUSTOM CONVERSION ---------------')
|
||||
UnitConversion.objects.create(
|
||||
base_amount=0,
|
||||
base_unit=unit_gram,
|
||||
converted_amount=0,
|
||||
converted_unit=unit_fantasy,
|
||||
space=space_1,
|
||||
created_by=auth.get_user(u1_s1),
|
||||
)
|
||||
conversions = uch.get_conversions(ingredient_food_1_gram)
|
||||
|
||||
assert len(conversions) == 1 # conversion always includes the ingredient, if count is 1 no other conversion was found
|
||||
|
||||
@@ -372,11 +372,16 @@ class MergeMixin(ViewSetMixin):
|
||||
isTree = False
|
||||
|
||||
try:
|
||||
# TODO these checks could be improved to merge existing properties and conversion in a smart way. For now it will just loose them to prevent duplicates
|
||||
if isinstance(source, Food):
|
||||
source.properties.all().delete()
|
||||
source.properties.clear()
|
||||
UnitConversion.objects.filter(food=source).delete()
|
||||
|
||||
if isinstance(source, Unit):
|
||||
UnitConversion.objects.filter(base_unit=source).delete()
|
||||
UnitConversion.objects.filter(converted_unit=source).delete()
|
||||
|
||||
for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]:
|
||||
linkManager = getattr(source, link.get_accessor_name())
|
||||
related = linkManager.all()
|
||||
@@ -1125,7 +1130,7 @@ class FoodViewSet(LoggingMixin, TreeMixin, DeleteRelationMixing):
|
||||
"type": "text",
|
||||
"text": "Given the following food and the following different types of properties please update the food so that the properties attribute contains a list with all property types in the following format [{property_amount: <the property value>, property_type: {id: <the ID of the property type>, name: <the name of the property type>}}]."
|
||||
"The property values should be in the unit given in the property type and for the amount specified in the properties_food_amount attribute of the food, which is given in the properties_food_unit."
|
||||
"property_amount is a decimal number. Please try to keep a percision of two decimal places if given in your source data."
|
||||
"property_amount is a decimal number. Please try to keep a precision of two decimal places if given in your source data."
|
||||
"Do not make up any data. If there is no data available for the given property type that is ok, just return null as a property_amount for that property type. Do not change anything else!"
|
||||
"Most property types are likely going to be nutritional values. Please do not make up any values, only return values you can find in the sources available to you."
|
||||
"Only return values if you are sure they are meant for the food given. Under no circumstance are you allowed to change any other value of the given food or change the structure in any way or form."
|
||||
@@ -1805,6 +1810,82 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet, DeleteRelationMixing):
|
||||
|
||||
return Response(serializer.errors, 400)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(name='provider', description='ID of the AI provider that should be used for this AI request', type=int),
|
||||
]
|
||||
)
|
||||
@decorators.action(detail=True, methods=['POST'], )
|
||||
def aiproperties(self, request, pk):
|
||||
serializer = RecipeSerializer(data=request.data, partial=True, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
|
||||
if not request.query_params.get('provider', None) or not re.match(r'^(\d)+$', request.query_params.get('provider', None)):
|
||||
response = {
|
||||
'error': True,
|
||||
'msg': _('You must select an AI provider to perform your request.'),
|
||||
}
|
||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not can_perform_ai_request(request.space):
|
||||
response = {
|
||||
'error': True,
|
||||
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
|
||||
}
|
||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ai_provider = AiProvider.objects.filter(pk=request.query_params.get('provider')).filter(Q(space=request.space) | Q(space__isnull=True)).first()
|
||||
|
||||
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider, AiLog.F_RECIPE_PROPERTIES)]
|
||||
|
||||
property_type_list = list(PropertyType.objects.filter(space=request.space).values('id', 'name', 'description', 'unit', 'category', 'fdc_id'))
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Given the following recipe and the following different types of properties please update the recipe so that the properties attribute contains a list with all property types in the following format [{property_amount: <the property value>, property_type: {id: <the ID of the property type>, name: <the name of the property type>}}]."
|
||||
"The property values should be in the unit given in the property type and calculated based on the total quantity of the foods used for the recipe."
|
||||
"property_amount is a decimal number. Please try to keep a precision of two decimal places if given in your source data."
|
||||
"Do not make up any data. If there is no data available for the given property type that is ok, just return null as a property_amount for that property type. Do not change anything else!"
|
||||
"Most property types are likely going to be nutritional values. Please do not make up any values, only return values you can find in the sources available to you."
|
||||
"Under no circumstance are you allowed to change any other value of the given food or change the structure in any way or form."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(request.data)
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(property_type_list)
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
ai_request = {
|
||||
'api_key': ai_provider.api_key,
|
||||
'model': ai_provider.model_name,
|
||||
'response_format': {"type": "json_object"},
|
||||
'messages': messages,
|
||||
}
|
||||
if ai_provider.url:
|
||||
ai_request['api_base'] = ai_provider.url
|
||||
ai_response = completion(**ai_request)
|
||||
|
||||
response_text = ai_response.choices[0].message.content
|
||||
|
||||
return Response(json.loads(response_text), status=status.HTTP_200_OK)
|
||||
except BadRequestError as err:
|
||||
pass
|
||||
response = {
|
||||
'error': True,
|
||||
'msg': 'The AI could not process your request. \n\n' + err.message,
|
||||
}
|
||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(responses=RecipeSerializer(many=False))
|
||||
@decorators.action(detail=True, pagination_class=None, methods=['PATCH'], serializer_class=RecipeSerializer)
|
||||
def delete_external(self, request, pk):
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<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/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></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/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
These instructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
|
||||
|
||||
!!! warning
|
||||
Make sure to use at least Python 3.10 (although 3.12 is preferred) or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`.
|
||||
Make sure to use at least Python 3.12 or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`.
|
||||
|
||||
!!! warning
|
||||
These instructions are **not** regularly reviewed and might be outdated.
|
||||
|
||||
@@ -96,12 +96,15 @@ Configuration options for serving related services.
|
||||
|
||||
#### Port
|
||||
|
||||
> default `8080` - options: `1-65535`
|
||||
> default `80` - options: `1-65535`
|
||||
|
||||
Port for gunicorn to bind to. Should not be changed if using docker stack with reverse proxy.
|
||||
!!! warning
|
||||
Changed in version 2.3 to no longer configure the port of gunicorn but the port of the internal nginx
|
||||
|
||||
Port where Tandoor exposes its internal web server.
|
||||
|
||||
```
|
||||
TANDOOR_PORT=8080
|
||||
TANDOOR_PORT=80
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80 ipv6only=on;
|
||||
listen ${TANDOOR_PORT};
|
||||
listen [::]:${TANDOOR_PORT} ipv6only=on;
|
||||
server_name localhost;
|
||||
|
||||
client_max_body_size 128M;
|
||||
@@ -19,7 +19,10 @@ server {
|
||||
# pass requests for dynamic content to gunicorn
|
||||
location / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_pass http://localhost:${TANDOOR_PORT};
|
||||
proxy_pass http://unix:/run/tandoor.sock;
|
||||
|
||||
# param needed by django allauth sessions to log IP
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# disabled for now because it redirects to the error page and not back, also not showing html
|
||||
#error_page 502 /errors/http502.html;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Django==5.2.6
|
||||
Django==5.2.7
|
||||
cryptography===45.0.5
|
||||
django-annoying==0.10.6
|
||||
django-cleanup==9.0.0
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<v-row>
|
||||
<v-col>
|
||||
<span v-if="step.name">{{ step.name }}</span>
|
||||
<span v-else-if="step.stepRecipe"><v-icon icon="$recipes"></v-icon> {{ step.stepRecipeData.name }}</span>
|
||||
<span v-else>{{ $t('Step') }} {{ props.stepNumber }}</span>
|
||||
</v-col>
|
||||
<v-col class="text-right">
|
||||
@@ -23,11 +22,12 @@
|
||||
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
|
||||
<v-card-text v-if="step.ingredients.length > 0 || step.instruction != ''">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && step.showIngredientsTable">
|
||||
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)">
|
||||
<ingredients-table v-model="step.ingredients" :ingredient-factor="ingredientFactor"></ingredients-table>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="markdown-body">
|
||||
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor" v-if="step.instructionsMarkdown != undefined"></instructions>
|
||||
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor"
|
||||
v-if="step.instructionsMarkdown != undefined"></instructions>
|
||||
<!-- sub recipes dont have a correct schema, thus they use different variable naming -->
|
||||
<instructions :instructions_html="step.instructions_markdown" :ingredient_factor="ingredientFactor" v-else></instructions>
|
||||
</v-col>
|
||||
@@ -35,7 +35,12 @@
|
||||
</v-card-text>
|
||||
|
||||
<template v-if="step.stepRecipe">
|
||||
<v-card class="ma-2 border-md" prepend-icon="$recipes" :title="step.stepRecipeData.name">
|
||||
<v-card class="ma-2 border-md">
|
||||
<v-card-title>
|
||||
<v-icon icon="$recipes"></v-icon>
|
||||
{{ step.stepRecipeData.name }}
|
||||
<v-btn icon="fa-solid fa-up-right-from-square" size="x-small" :to="{name: 'RecipeViewPage', params: {id: step.stepRecipeData.id}}" target="_blank" variant="plain"></v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="mt-1" v-for="(subRecipeStep, subRecipeStepIndex) in step.stepRecipeData.steps" :key="subRecipeStep.id">
|
||||
<step-view v-model="step.stepRecipeData.steps[subRecipeStepIndex]" :step-number="subRecipeStepIndex+1" :ingredientFactor="ingredientFactor"></step-view>
|
||||
</v-card-text>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-btn-group density="compact">
|
||||
<v-btn color="create" @click="food.properties.push({} as Property)" prepend-icon="$create">{{ $t('Add') }}</v-btn>
|
||||
<v-btn color="create" @click="editingObj.properties.push({} as Property)" prepend-icon="$create">{{ $t('Add') }}</v-btn>
|
||||
<v-btn color="secondary" @click="addAllProperties" prepend-icon="fa-solid fa-list">{{ $t('AddAll') }}</v-btn>
|
||||
<ai-action-button color="info" @selected="propertiesFromAi" :loading="aiLoading" prepend-icon="$ai">{{ $t('AI') }}</ai-action-button>
|
||||
</v-btn-group>
|
||||
|
||||
<v-row class="d-none d-md-flex mt-2" v-for="p in food.properties" dense>
|
||||
<v-row class="d-none d-md-flex mt-2" v-for="p in editingObj.properties" dense>
|
||||
<v-col cols="0" md="6">
|
||||
<v-number-input :step="10" v-model="p.propertyAmount" control-variant="stacked" :precision="2">
|
||||
<template #append-inner v-if="p.propertyType">
|
||||
@@ -25,7 +25,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-list class="d-md-none">
|
||||
<v-list-item v-for="p in food.properties" border>
|
||||
<v-list-item v-for="p in editingObj.properties" border>
|
||||
<span v-if="p.propertyType">{{ p.propertyAmount }} {{ p.propertyType.unit }} {{ p.propertyType.name }} / {{ props.amountFor }}
|
||||
</span>
|
||||
<span v-else><i><{{ $t('New') }}></i></span>
|
||||
@@ -41,10 +41,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ApiApi, Food, Property} from "@/openapi";
|
||||
import {ApiApi, Food, Property, Recipe} from "@/openapi";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import {PropType, ref} from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import AiActionButton from "@/components/buttons/AiActionButton.vue";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
|
||||
|
||||
@@ -52,7 +52,11 @@ const props = defineProps({
|
||||
amountFor: {type: String, required: true},
|
||||
})
|
||||
|
||||
const food = defineModel<Food>({required: true})
|
||||
const isFood = computed(() => {
|
||||
return !('steps' in editingObj.value)
|
||||
})
|
||||
|
||||
const editingObj = defineModel<Food | Recipe>({required: true})
|
||||
|
||||
const aiLoading = ref(false)
|
||||
|
||||
@@ -61,8 +65,8 @@ const aiLoading = ref(false)
|
||||
* @param property property to delete
|
||||
*/
|
||||
function deleteProperty(property: Property) {
|
||||
if (food.value.properties) {
|
||||
food.value.properties = food.value.properties.filter(p => p !== property)
|
||||
if (editingObj.value.properties) {
|
||||
editingObj.value.properties = editingObj.value.properties.filter(p => p !== property)
|
||||
// TODO delete from DB, needs endpoint for property relation to either recipe or food
|
||||
}
|
||||
}
|
||||
@@ -74,14 +78,14 @@ function deleteProperty(property: Property) {
|
||||
function addAllProperties() {
|
||||
const api = new ApiApi()
|
||||
|
||||
if (food.value.properties) {
|
||||
food.value.properties = []
|
||||
if (editingObj.value.properties) {
|
||||
editingObj.value.properties = []
|
||||
}
|
||||
|
||||
api.apiPropertyTypeList().then(r => {
|
||||
r.results.forEach(pt => {
|
||||
if (food.value.properties.findIndex(x => x.propertyType.name == pt.name) == -1) {
|
||||
food.value.properties.push({propertyAmount: 0, propertyType: pt} as Property)
|
||||
if (editingObj.value.properties.findIndex(x => x.propertyType.name == pt.name) == -1) {
|
||||
editingObj.value.properties.push({propertyAmount: 0, propertyType: pt} as Property)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -90,13 +94,25 @@ function addAllProperties() {
|
||||
function propertiesFromAi(providerId: number) {
|
||||
const api = new ApiApi()
|
||||
aiLoading.value = true
|
||||
api.apiFoodAipropertiesCreate({id: food.value.id!, food: food.value, provider: providerId}).then(r => {
|
||||
food.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}).finally(() => {
|
||||
aiLoading.value = false
|
||||
})
|
||||
|
||||
if (isFood.value) {
|
||||
api.apiFoodAipropertiesCreate({id: editingObj.value.id!, food: editingObj.value, provider: providerId}).then(r => {
|
||||
editingObj.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}).finally(() => {
|
||||
aiLoading.value = false
|
||||
})
|
||||
} else {
|
||||
api.apiRecipeAipropertiesCreate({id: editingObj.value.id!, recipe: editingObj.value, provider: providerId}).then(r => {
|
||||
editingObj.value = r
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}).finally(() => {
|
||||
aiLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
<div class="d-flex flex-nowrap">
|
||||
<div class="flex-col flex-grow-0 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader">
|
||||
<v-text-field :id="`id_input_amount_${step.id}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact"
|
||||
<v-text-field :id="`id_input_amount_${props.stepIndex}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact"
|
||||
hide-details :disabled="ingredient.noAmount">
|
||||
|
||||
<template #prepend>
|
||||
@@ -261,24 +261,6 @@ const dialogIngredientSorter = ref(false)
|
||||
const editingIngredientIndex = ref(0)
|
||||
const ingredientTextInput = ref("")
|
||||
|
||||
const defaultUnit = ref<null | Unit>(null)
|
||||
|
||||
onMounted(() => {
|
||||
let api = new ApiApi()
|
||||
|
||||
if (useUserPreferenceStore().userSettings.defaultUnit) {
|
||||
api.apiUnitList({query: useUserPreferenceStore().userSettings.defaultUnit}).then(r => {
|
||||
r.results.forEach(u => {
|
||||
if (u.name == useUserPreferenceStore().userSettings.defaultUnit) {
|
||||
defaultUnit.value = u
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* sort function called by draggable when ingredient table is sorted
|
||||
*/
|
||||
@@ -334,14 +316,10 @@ function handleIngredientNoteTab(event: KeyboardEvent, index: number) {
|
||||
function insertAndFocusIngredient() {
|
||||
let ingredient = {
|
||||
amount: 0,
|
||||
unit: null,
|
||||
unit: useUserPreferenceStore().defaultUnitObj,
|
||||
food: null,
|
||||
} as Ingredient
|
||||
|
||||
if (defaultUnit.value != null) {
|
||||
ingredient.unit = defaultUnit.value
|
||||
}
|
||||
|
||||
step.value.ingredients.push(ingredient)
|
||||
nextTick(() => {
|
||||
sortIngredients()
|
||||
@@ -349,7 +327,7 @@ function insertAndFocusIngredient() {
|
||||
editingIngredientIndex.value = step.value.ingredients.length - 1
|
||||
dialogIngredientEditor.value = true
|
||||
} else {
|
||||
document.getElementById(`id_input_amount_${step.value.id}_${step.value.ingredients.length - 1}`).select()
|
||||
document.getElementById(`id_input_amount_${props.stepIndex}_${step.value.ingredients.length - 1}`).select()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<v-rating v-model="editingObj.rating" clearable hover density="compact"></v-rating>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
|
||||
<v-number-input :label="$t('Servings')" v-model="editingObj.servings" :precision="2"></v-number-input>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
@@ -42,7 +41,7 @@ import {onMounted, PropType, watch} from "vue";
|
||||
import {CookLog} from "@/openapi";
|
||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||
|
||||
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
|
||||
|
||||
const props = defineProps({
|
||||
item: {type: {} as PropType<CookLog>, required: false, default: null},
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<v-tabs v-model="tab" :disabled="loading || fileApiLoading" grow>
|
||||
<v-tab value="recipe">{{ $t('Recipe') }}</v-tab>
|
||||
<v-tab value="steps">{{ $t('Steps') }}</v-tab>
|
||||
<v-tab value="properties">{{ $t('Properties') }}</v-tab>
|
||||
<v-tab value="settings">{{ $t('Miscellaneous') }}</v-tab>
|
||||
<v-tab value="properties" :disabled="!isUpdate()">{{ $t('Properties') }}</v-tab>
|
||||
<v-tab value="settings" :disabled="!isUpdate()">{{ $t('Miscellaneous') }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="!isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
|
||||
@@ -106,7 +106,10 @@
|
||||
<v-tabs-window-item value="properties">
|
||||
<v-form :disabled="loading || fileApiLoading">
|
||||
<closable-help-alert :text="$t('PropertiesFoodHelp')"></closable-help-alert>
|
||||
<properties-editor v-model="editingObj.properties" :amount-for="$t('Serving')"></properties-editor>
|
||||
<properties-editor v-model="editingObj" :amount-for="$t('Serving')"></properties-editor>
|
||||
|
||||
<!-- TODO remove once append to body for model select is working properly -->
|
||||
<v-spacer style="margin-top: 100px;"></v-spacer>
|
||||
</v-form>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="settings">
|
||||
@@ -226,7 +229,7 @@ function initializeEditor() {
|
||||
addStep()
|
||||
editingObj.value.steps[0].ingredients.push({
|
||||
food: null,
|
||||
unit: null,
|
||||
unit: useUserPreferenceStore().defaultUnitObj,
|
||||
amount: 0,
|
||||
} as Ingredient)
|
||||
editingObj.value.internal = true //TODO make database default after v2
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col md="6">
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.baseAmount" control-variant="stacked" :precision="3"></v-number-input>
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.baseAmount" control-variant="stacked" :precision="3" :min="0.001"></v-number-input>
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select :label="$t('Unit')" v-model="editingObj.baseUnit" model="Unit"></model-select>
|
||||
<model-select v-model="editingObj.baseUnit" model="Unit"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-0">
|
||||
@@ -33,11 +33,11 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col md="6">
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.convertedAmount" control-variant="stacked" :precision="3"></v-number-input>
|
||||
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.convertedAmount" control-variant="stacked" :precision="3" :min="0.001"></v-number-input>
|
||||
</v-col>
|
||||
<v-col md="6">
|
||||
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
|
||||
<model-select :label="$t('Unit')" v-model="editingObj.convertedUnit" model="Unit"></model-select>
|
||||
<model-select v-model="editingObj.convertedUnit" model="Unit"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"Fats": "",
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Finish": "",
|
||||
"Food": "",
|
||||
"FoodInherit": "",
|
||||
"FoodNotOnHand": "",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"Fats": "Мазнини",
|
||||
"File": "Файл",
|
||||
"Files": "Файлове",
|
||||
"Finish": "",
|
||||
"Food": "Храна",
|
||||
"FoodInherit": "Хранителни наследствени полета",
|
||||
"FoodNotOnHand": "Нямате {храна} под ръка.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "Greixos",
|
||||
"File": "Arxiu",
|
||||
"Files": "Arxius",
|
||||
"Finish": "",
|
||||
"First_name": "Nom",
|
||||
"Food": "Aliment",
|
||||
"FoodInherit": "Camps Heretats",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"Fats": "Tuky",
|
||||
"File": "Soubor",
|
||||
"Files": "Soubory",
|
||||
"Finish": "",
|
||||
"First_name": "Jméno",
|
||||
"Food": "Potravina",
|
||||
"FoodInherit": "Propisovatelná pole potraviny",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "Fedtstoffer",
|
||||
"File": "Fil",
|
||||
"Files": "Filer",
|
||||
"Finish": "",
|
||||
"First_name": "Fornavn",
|
||||
"Food": "Mad",
|
||||
"FoodInherit": "Nedarvelige mad felter",
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
"Fats": "Fette",
|
||||
"File": "Datei",
|
||||
"Files": "Dateien",
|
||||
"Finish": "Fertigstellen",
|
||||
"FinishedAt": "Fertig um",
|
||||
"First": "Erstes",
|
||||
"First_name": "Vorname",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "Λιπαρά",
|
||||
"File": "Αρχείο",
|
||||
"Files": "Αρχεία",
|
||||
"Finish": "",
|
||||
"First_name": "Όνομα",
|
||||
"Food": "Φαγητό",
|
||||
"FoodInherit": "Πεδία φαγητών που κληρονομούνται",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"Fats": "Fats",
|
||||
"File": "File",
|
||||
"Files": "Files",
|
||||
"Finish": "Finish",
|
||||
"FinishedAt": "Finished at",
|
||||
"First": "First",
|
||||
"First_name": "First Name",
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"Fats": "Grasas",
|
||||
"File": "Archivo",
|
||||
"Files": "Archivos",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Finaliza a las",
|
||||
"First": "Primero",
|
||||
"First_name": "Nombre",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"Fats": "Rasvat",
|
||||
"File": "Tiedosto",
|
||||
"Files": "Tiedostot",
|
||||
"Finish": "",
|
||||
"First_name": "Etunimi",
|
||||
"Food": "Ruoka",
|
||||
"FoodInherit": "Ruoan perinnölliset kentät",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"Fats": "Matières grasses",
|
||||
"File": "Fichier",
|
||||
"Files": "Fichiers",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Terminé à",
|
||||
"First": "Premier",
|
||||
"First_name": "Prénom",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "שומנים",
|
||||
"File": "קובץ",
|
||||
"Files": "קבצים",
|
||||
"Finish": "",
|
||||
"First_name": "שם פרטי",
|
||||
"Food": "אוכל",
|
||||
"FoodInherit": "ערכי מזון",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "Masti",
|
||||
"File": "Datoteka",
|
||||
"Files": "Datoteke",
|
||||
"Finish": "",
|
||||
"First_name": "Ime",
|
||||
"Food": "Namirnica",
|
||||
"FoodInherit": "Nasljedna polja namirnice",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"Fats": "Zsírok",
|
||||
"File": "Fájl",
|
||||
"Files": "Fájlok",
|
||||
"Finish": "",
|
||||
"First_name": "Keresztnév",
|
||||
"Food": "Alapanyag",
|
||||
"FoodInherit": "",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"Fats": "",
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Finish": "",
|
||||
"Food": "Սննդամթերք",
|
||||
"FromBalance": "",
|
||||
"Fulltext": "",
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"Fats": "Lemak",
|
||||
"File": "Berkas",
|
||||
"Files": "File",
|
||||
"Finish": "",
|
||||
"First_name": "",
|
||||
"Food": "",
|
||||
"FoodInherit": "",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"Fats": "",
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Finish": "",
|
||||
"First_name": "",
|
||||
"Food": "",
|
||||
"FoodInherit": "",
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
"Fats": "Grassi",
|
||||
"File": "File",
|
||||
"Files": "File",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Finito alle",
|
||||
"First": "Primo",
|
||||
"First_name": "Nome",
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"Fats": "",
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Finish": "",
|
||||
"First_name": "",
|
||||
"Food": "",
|
||||
"FoodInherit": "",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "",
|
||||
"File": "",
|
||||
"Files": "",
|
||||
"Finish": "",
|
||||
"First_name": "",
|
||||
"Food": "",
|
||||
"FoodInherit": "",
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"Fats": "Fett",
|
||||
"File": "Fil",
|
||||
"Files": "Filer",
|
||||
"Finish": "",
|
||||
"First_name": "Fornavn",
|
||||
"Food": "Matretter",
|
||||
"FoodInherit": "Arvbare felt for matvarer",
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
"Fats": "Vetten",
|
||||
"File": "Bestand",
|
||||
"Files": "Bestanden",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Afgerond op",
|
||||
"First": "Eerste",
|
||||
"First_name": "Voornaam",
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"Fats": "Tłuszcze",
|
||||
"File": "Plik",
|
||||
"Files": "Pliki",
|
||||
"Finish": "",
|
||||
"First_name": "Imię",
|
||||
"Food": "Żywność",
|
||||
"FoodInherit": "Pola dziedziczone w żywności",
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"Fats": "Gorduras",
|
||||
"File": "Ficheiro",
|
||||
"Files": "Ficheiros",
|
||||
"Finish": "",
|
||||
"Food": "Comida",
|
||||
"FoodInherit": "Campos herdados por comida",
|
||||
"FoodNotOnHand": "Não têm {food} disponível.",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"Fats": "Gorduras",
|
||||
"File": "Arquivo",
|
||||
"Files": "Arquivos",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Finalizado em",
|
||||
"First": "Primeiro",
|
||||
"First_name": "Primeiro Nome",
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
"Fats": "Grăsimi",
|
||||
"File": "Fișier",
|
||||
"Files": "Fișiere",
|
||||
"Finish": "",
|
||||
"First_name": "Prenume",
|
||||
"Food": "Mâncare",
|
||||
"FoodInherit": "Câmpuri moștenite de alimente",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"Fats": "Жиры",
|
||||
"File": "Файл",
|
||||
"Files": "Файлы",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Завершено в",
|
||||
"First": "Первый",
|
||||
"First_name": "Имя",
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
"Fats": "Maščobe",
|
||||
"File": "Datoteka",
|
||||
"Files": "Datoteke",
|
||||
"Finish": "",
|
||||
"FinishedAt": "Končano ob",
|
||||
"First": "Prvi",
|
||||
"First_name": "Ime",
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
"Fats": "Fett",
|
||||
"File": "Fil",
|
||||
"Files": "Filer",
|
||||
"Finish": "",
|
||||
"First_name": "Förnamn",
|
||||
"Food": "Livsmedel",
|
||||
"FoodInherit": "Ärftliga livsmedels fält",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "Yağlar",
|
||||
"File": "Dosya",
|
||||
"Files": "Dosyalar",
|
||||
"Finish": "",
|
||||
"First_name": "İsim",
|
||||
"Food": "Yiyecek",
|
||||
"FoodInherit": "Yiyeceğin Devralınabileceği Alanlar",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"Fats": "Жири",
|
||||
"File": "Файл",
|
||||
"Files": "Файли",
|
||||
"Finish": "",
|
||||
"Food": "Їжа",
|
||||
"FoodInherit": "Пола Успадкованої Їжі",
|
||||
"FoodNotOnHand": "У вас немає {food} на руках.",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Fats": "脂肪",
|
||||
"File": "文件",
|
||||
"Files": "文件",
|
||||
"Finish": "",
|
||||
"First_name": "名",
|
||||
"Food": "食物",
|
||||
"FoodInherit": "食物可继承的字段",
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
"Fats": "脂肪",
|
||||
"File": "檔案",
|
||||
"Files": "檔案",
|
||||
"Finish": "",
|
||||
"FinishedAt": "完成於",
|
||||
"First": "第一個",
|
||||
"First_name": "名字",
|
||||
|
||||
@@ -877,6 +877,12 @@ export interface ApiEnterpriseSocialKeywordUpdateRequest {
|
||||
keyword: Omit<Keyword, 'label'|'parent'|'numchild'|'createdAt'|'updatedAt'|'fullName'>;
|
||||
}
|
||||
|
||||
export interface ApiEnterpriseSocialRecipeAipropertiesCreateRequest {
|
||||
id: number;
|
||||
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
|
||||
provider?: number;
|
||||
}
|
||||
|
||||
export interface ApiEnterpriseSocialRecipeBatchUpdateUpdateRequest {
|
||||
recipeBatchUpdate: RecipeBatchUpdate;
|
||||
}
|
||||
@@ -1689,6 +1695,12 @@ export interface ApiPropertyUpdateRequest {
|
||||
property: Property;
|
||||
}
|
||||
|
||||
export interface ApiRecipeAipropertiesCreateRequest {
|
||||
id: number;
|
||||
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
|
||||
provider?: number;
|
||||
}
|
||||
|
||||
export interface ApiRecipeBatchUpdateUpdateRequest {
|
||||
recipeBatchUpdate: RecipeBatchUpdate;
|
||||
}
|
||||
@@ -5574,6 +5586,57 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
async apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
|
||||
if (requestParameters['id'] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
'id',
|
||||
'Required parameter "id" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().'
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters['recipe'] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
'recipe',
|
||||
'Required parameter "recipe" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().'
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
if (requestParameters['provider'] != null) {
|
||||
queryParameters['provider'] = requestParameters['provider'];
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters['Content-Type'] = 'application/json';
|
||||
|
||||
if (this.configuration && this.configuration.apiKey) {
|
||||
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
|
||||
}
|
||||
|
||||
const response = await this.request({
|
||||
path: `/api/enterprise-social-recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
|
||||
method: 'POST',
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: RecipeToJSON(requestParameters['recipe']),
|
||||
}, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
async apiEnterpriseSocialRecipeAipropertiesCreate(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
|
||||
const response = await this.apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
@@ -12351,6 +12414,57 @@ export class ApiApi extends runtime.BaseAPI {
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
async apiRecipeAipropertiesCreateRaw(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
|
||||
if (requestParameters['id'] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
'id',
|
||||
'Required parameter "id" was null or undefined when calling apiRecipeAipropertiesCreate().'
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters['recipe'] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
'recipe',
|
||||
'Required parameter "recipe" was null or undefined when calling apiRecipeAipropertiesCreate().'
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
if (requestParameters['provider'] != null) {
|
||||
queryParameters['provider'] = requestParameters['provider'];
|
||||
}
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters['Content-Type'] = 'application/json';
|
||||
|
||||
if (this.configuration && this.configuration.apiKey) {
|
||||
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
|
||||
}
|
||||
|
||||
const response = await this.request({
|
||||
path: `/api/recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
|
||||
method: 'POST',
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: RecipeToJSON(requestParameters['recipe']),
|
||||
}, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
async apiRecipeAipropertiesCreate(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
|
||||
const response = await this.apiRecipeAipropertiesCreateRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* logs request counts to redis cache total/per user/
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</v-list-item>
|
||||
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedFood">
|
||||
{{ $t('Merge') }}
|
||||
<model-merge-dialog :source="selectedFood" model="Food"
|
||||
<model-merge-dialog :source="[selectedFood]" model="Food"
|
||||
@change="(obj: Food) => {selectedFood = obj;refreshPage()} "></model-merge-dialog>
|
||||
</v-list-item>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</v-list-item>
|
||||
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedUnit">
|
||||
{{ $t('Merge') }}
|
||||
<model-merge-dialog :source="selectedUnit" model="Unit"
|
||||
<model-merge-dialog :source="[selectedUnit]" model="Unit"
|
||||
@change="(obj: Food) => {selectedUnit = obj;refreshPage()} "></model-merge-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item link prepend-icon="$automation" :disabled="!selectedUnit">
|
||||
@@ -117,12 +117,12 @@
|
||||
@update:modelValue="item.changed = true" :precision="2"></v-number-input>
|
||||
</template>
|
||||
<template v-slot:item.unit="{ item }">
|
||||
<model-select model="Unit" v-model="item.unit" :label="$t('Unit')" density="compact" hide-details allow-create append-to-body
|
||||
<model-select model="Unit" v-model="item.unit" density="compact" hide-details allow-create append-to-body
|
||||
@update:modelValue="item.changed = true">
|
||||
</model-select>
|
||||
</template>
|
||||
<template v-slot:item.food="{ item }">
|
||||
<model-select model="Food" v-model="item.food" :label="$t('Food')" density="compact" hide-details allow-create append-to-body
|
||||
<model-select model="Food" v-model="item.food" density="compact" hide-details allow-create append-to-body
|
||||
@update:modelValue="item.changed = true"></model-select>
|
||||
</template>
|
||||
<template v-slot:item.note="{ item }">
|
||||
|
||||
@@ -837,7 +837,7 @@ function deleteStep(step: SourceImportStep) {
|
||||
|
||||
function handleMergeAllSteps(): void {
|
||||
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
|
||||
mergeAllSteps(importResponse.value.recipe.steps)
|
||||
importResponse.value.recipe.steps = mergeAllSteps(importResponse.value.recipe.steps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,7 +931,10 @@ function setAllKeywordsImportStatus(status: boolean) {
|
||||
* add a new (empty) step at the end of the step list
|
||||
*/
|
||||
function addStep() {
|
||||
importResponse.value.recipe?.steps.push({} as SourceImportStep)
|
||||
importResponse.value.recipe?.steps.push({
|
||||
ingredients: [],
|
||||
instruction: ''
|
||||
} as SourceImportStep)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -788,7 +788,7 @@ const filters = ref({
|
||||
enabled: false,
|
||||
default: undefined,
|
||||
is: VNumberInput,
|
||||
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}),
|
||||
modelValue: useRouteQuery('timescooked', undefined, {transform: Number}),
|
||||
},
|
||||
timescookedGte: {
|
||||
id: 'timescookedGte',
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
</v-card>
|
||||
<template v-if="totalRecipes > 0">
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="recent" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="new"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="new" v-if="totalRecipes > 1"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 1"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="created_by" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="2" mode="rating" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
|
||||
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import {useStorage} from "@vueuse/core";
|
||||
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
|
||||
import {ApiApi, ServerSettings, Space, UserPreference, UserSpace} from "@/openapi";
|
||||
import {ApiApi, ServerSettings, Space, Unit, UserPreference, UserSpace} from "@/openapi";
|
||||
import {ShoppingGroupingOptions} from "@/types/Shopping";
|
||||
import {computed, ComputedRef, ref} from "vue";
|
||||
import {DeviceSettings} from "@/types/settings";
|
||||
@@ -50,6 +50,11 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
||||
*/
|
||||
const initCompleted = ref(false)
|
||||
|
||||
/**
|
||||
* load the default unit to the store for easy use in editors and more
|
||||
*/
|
||||
const defaultUnitObj = ref<Unit | null>(null)
|
||||
|
||||
const theme = useTheme()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -77,6 +82,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
||||
userSettings.value = r[0]
|
||||
isAuthenticated.value = true
|
||||
updateTheme()
|
||||
loadDefaultUnit()
|
||||
} else {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, r)
|
||||
}
|
||||
@@ -87,6 +93,28 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* load the default unit from the backend
|
||||
* TODO migrate to nested serializer but requires actually creating the unit as currently its possible the default unit does not exist yet
|
||||
*/
|
||||
function loadDefaultUnit() {
|
||||
let api = new ApiApi()
|
||||
|
||||
if (userSettings.value.defaultUnit) {
|
||||
api.apiUnitList({query: userSettings.value.defaultUnit}).then(r => {
|
||||
r.results.forEach(u => {
|
||||
if (u.name == userSettings.value.defaultUnit) {
|
||||
defaultUnitObj.value = u
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
if (err.response.status != 403) {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* persist changes to user settings to DB
|
||||
*/
|
||||
@@ -254,6 +282,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
|
||||
activeUserSpace,
|
||||
isAuthenticated,
|
||||
initCompleted,
|
||||
defaultUnitObj,
|
||||
loadUserSettings,
|
||||
loadServerSettings,
|
||||
updateUserSettings,
|
||||
|
||||
Reference in New Issue
Block a user