sandwitches 1.4.2__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sandwitches/__init__.py +6 -0
- sandwitches/admin.py +21 -2
- sandwitches/api.py +112 -6
- sandwitches/feeds.py +23 -0
- sandwitches/forms.py +110 -7
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +784 -134
- sandwitches/migrations/0001_initial.py +255 -2
- sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
- sandwitches/migrations/0003_setting.py +35 -0
- sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
- sandwitches/migrations/0005_rating_comment.py +17 -0
- sandwitches/models.py +48 -4
- sandwitches/settings.py +14 -5
- sandwitches/storage.py +44 -12
- sandwitches/templates/admin/admin_base.html +118 -0
- sandwitches/templates/admin/confirm_delete.html +23 -0
- sandwitches/templates/admin/dashboard.html +262 -0
- sandwitches/templates/admin/rating_list.html +38 -0
- sandwitches/templates/admin/recipe_form.html +184 -0
- sandwitches/templates/admin/recipe_list.html +64 -0
- sandwitches/templates/admin/tag_form.html +30 -0
- sandwitches/templates/admin/tag_list.html +37 -0
- sandwitches/templates/admin/task_detail.html +91 -0
- sandwitches/templates/admin/task_list.html +41 -0
- sandwitches/templates/admin/user_form.html +37 -0
- sandwitches/templates/admin/user_list.html +60 -0
- sandwitches/templates/base.html +80 -1
- sandwitches/templates/base_beer.html +57 -0
- sandwitches/templates/components/favorites_search_form.html +85 -0
- sandwitches/templates/components/footer.html +14 -0
- sandwitches/templates/components/ingredients_scripts.html +50 -0
- sandwitches/templates/components/ingredients_section.html +11 -0
- sandwitches/templates/components/instructions_section.html +9 -0
- sandwitches/templates/components/language_dialog.html +26 -0
- sandwitches/templates/components/navbar.html +27 -0
- sandwitches/templates/components/rating_section.html +66 -0
- sandwitches/templates/components/recipe_header.html +32 -0
- sandwitches/templates/components/search_form.html +106 -0
- sandwitches/templates/components/search_scripts.html +98 -0
- sandwitches/templates/components/side_menu.html +31 -0
- sandwitches/templates/components/user_menu.html +10 -0
- sandwitches/templates/detail.html +167 -110
- sandwitches/templates/favorites.html +42 -0
- sandwitches/templates/index.html +28 -61
- sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches/templates/recipe_form.html +119 -0
- sandwitches/templates/setup.html +1 -1
- sandwitches/templates/signup.html +114 -31
- sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches/urls.py +56 -0
- sandwitches/utils.py +222 -0
- sandwitches/views.py +503 -14
- sandwitches-1.5.0.dist-info/METADATA +104 -0
- sandwitches-1.5.0.dist-info/RECORD +62 -0
- sandwitches/migrations/0002_historicalrecipe.py +0 -61
- sandwitches/migrations/0003_rating.py +0 -57
- sandwitches/migrations/0004_add_uploaded_by.py +0 -25
- sandwitches/migrations/0005_historicalrecipe_uploaded_by.py +0 -27
- sandwitches/migrations/0006_profile.py +0 -48
- sandwitches/migrations/0007_alter_rating_score.py +0 -23
- sandwitches/migrations/0008_delete_profile.py +0 -15
- sandwitches/templates/base_pico.html +0 -260
- sandwitches/templates/form.html +0 -16
- sandwitches-1.4.2.dist-info/METADATA +0 -25
- sandwitches-1.4.2.dist-info/RECORD +0 -35
- {sandwitches-1.4.2.dist-info → sandwitches-1.5.0.dist-info}/WHEEL +0 -0
|
@@ -1,50 +1,133 @@
|
|
|
1
|
-
{% extends "
|
|
1
|
+
{% extends "base_beer.html" %}
|
|
2
2
|
{% load static i18n %}
|
|
3
3
|
{% block title %}{% trans "Sign Up" %}{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block content %}
|
|
6
|
-
<div class="
|
|
7
|
-
<article class="card">
|
|
8
|
-
<div class="card-body">
|
|
9
|
-
<h2>{% trans "Sign up" %}</h2>
|
|
6
|
+
<div class="large-space"></div>
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
<div class="grid">
|
|
9
|
+
<div class="s12 m10 l8 xl6 middle-align center-align" style="margin: 0 auto;">
|
|
10
|
+
<article class="round elevate">
|
|
11
|
+
<div class="padding">
|
|
12
|
+
<h4 class="center-align primary-text">{% trans "Create your account" %}</h4>
|
|
13
|
+
<p class="center-align">{% trans "Join the Sandwitches community today!" %}</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<form method="post" enctype="multipart/form-data" novalidate>
|
|
12
17
|
{% csrf_token %}
|
|
18
|
+
|
|
13
19
|
{% if form.non_field_errors %}
|
|
14
|
-
<div class="
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
<div class="padding error surface round mb">
|
|
21
|
+
{% for err in form.non_field_errors %}
|
|
22
|
+
<div class="row align-center">
|
|
23
|
+
<i class="error-text">warning</i>
|
|
24
|
+
<span class="error-text">{{ err }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
{% endfor %}
|
|
20
27
|
</div>
|
|
21
28
|
{% endif %}
|
|
22
29
|
|
|
23
|
-
<
|
|
24
|
-
|
|
30
|
+
<div class="grid">
|
|
31
|
+
<div class="s12 m6">
|
|
32
|
+
<div class="field label border round {% if form.first_name.errors %}error{% endif %}">
|
|
33
|
+
<input type="text" name="{{ form.first_name.name }}" id="{{ form.first_name.id_for_label }}" value="{{ form.first_name.value|default:'' }}">
|
|
34
|
+
<label>{% trans "First name" %}</label>
|
|
35
|
+
{% if form.first_name.errors %}
|
|
36
|
+
<span class="helper error-text">{{ form.first_name.errors.0 }}</span>
|
|
37
|
+
{% endif %}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="s12 m6">
|
|
41
|
+
<div class="field label border round {% if form.last_name.errors %}error{% endif %}">
|
|
42
|
+
<input type="text" name="{{ form.last_name.name }}" id="{{ form.last_name.id_for_label }}" value="{{ form.last_name.value|default:'' }}">
|
|
43
|
+
<label>{% trans "Last name" %}</label>
|
|
44
|
+
{% if form.last_name.errors %}
|
|
45
|
+
<span class="helper error-text">{{ form.last_name.errors.0 }}</span>
|
|
46
|
+
{% endif %}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
25
50
|
|
|
26
|
-
<
|
|
27
|
-
|
|
51
|
+
<div class="field label border round {% if form.username.errors %}error{% endif %}">
|
|
52
|
+
<input type="text" name="{{ form.username.name }}" id="{{ form.username.id_for_label }}" value="{{ form.username.value|default:'' }}">
|
|
53
|
+
<label>{% trans "Username" %}</label>
|
|
54
|
+
{% if form.username.errors %}
|
|
55
|
+
<span class="helper error-text">{{ form.username.errors.0 }}</span>
|
|
56
|
+
{% endif %}
|
|
57
|
+
</div>
|
|
28
58
|
|
|
29
|
-
<
|
|
30
|
-
|
|
59
|
+
<div class="field label border round {% if form.email.errors %}error{% endif %}">
|
|
60
|
+
<input type="email" name="{{ form.email.name }}" id="{{ form.email.id_for_label }}" value="{{ form.email.value|default:'' }}">
|
|
61
|
+
<label>{% trans "Email (optional)" %}</label>
|
|
62
|
+
{% if form.email.errors %}
|
|
63
|
+
<span class="helper error-text">{{ form.email.errors.0 }}</span>
|
|
64
|
+
{% endif %}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="field label border round {% if form.language.errors %}error{% endif %}">
|
|
68
|
+
<select name="{{ form.language.name }}" id="{{ form.language.id_for_label }}">
|
|
69
|
+
{% for value, label in form.language.field.choices %}
|
|
70
|
+
<option value="{{ value }}" {% if form.language.value == value %}selected{% endif %}>{{ label }}</option>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</select>
|
|
73
|
+
<label>{% trans "Preferred Language" %}</label>
|
|
74
|
+
{% if form.language.errors %}
|
|
75
|
+
<span class="helper error-text">{{ form.language.errors.0 }}</span>
|
|
76
|
+
{% endif %}
|
|
77
|
+
</div>
|
|
31
78
|
|
|
32
|
-
<
|
|
33
|
-
|
|
79
|
+
<div class="field label border round textarea {% if form.bio.errors %}error{% endif %}">
|
|
80
|
+
<textarea name="{{ form.bio.name }}" id="{{ form.bio.id_for_label }}" rows="3">{{ form.bio.value|default:'' }}</textarea>
|
|
81
|
+
<label>{% trans "Bio" %}</label>
|
|
82
|
+
{% if form.bio.errors %}
|
|
83
|
+
<span class="helper error-text">{{ form.bio.errors.0 }}</span>
|
|
84
|
+
{% endif %}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="field middle-align {% if form.avatar.errors %}error{% endif %}">
|
|
88
|
+
<nav>
|
|
89
|
+
<div class="max">
|
|
90
|
+
<label class="button border transparent round">
|
|
91
|
+
<input type="file" name="{{ form.avatar.name }}" id="{{ form.avatar.id_for_label }}" accept="image/*">
|
|
92
|
+
<i>upload</i>
|
|
93
|
+
<span>{% trans "Upload Profile Picture" %}</span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
</nav>
|
|
97
|
+
{% if form.avatar.errors %}
|
|
98
|
+
<span class="helper error-text">{{ form.avatar.errors.0 }}</span>
|
|
99
|
+
{% endif %}
|
|
100
|
+
</div>
|
|
34
101
|
|
|
35
|
-
<
|
|
36
|
-
|
|
102
|
+
<div class="space"></div>
|
|
103
|
+
<div class="divider"></div>
|
|
104
|
+
<div class="space"></div>
|
|
37
105
|
|
|
38
|
-
<
|
|
39
|
-
|
|
106
|
+
<div class="field label border round {% if form.password1.errors %}error{% endif %}">
|
|
107
|
+
<input type="password" name="{{ form.password1.name }}" id="{{ form.password1.id_for_label }}">
|
|
108
|
+
<label>{% trans "Password" %}</label>
|
|
109
|
+
{% if form.password1.errors %}
|
|
110
|
+
<span class="helper error-text">{{ form.password1.errors.0 }}</span>
|
|
111
|
+
{% endif %}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="field label border round {% if form.password2.errors %}error{% endif %}">
|
|
115
|
+
<input type="password" name="{{ form.password2.name }}" id="{{ form.password2.id_for_label }}">
|
|
116
|
+
<label>{% trans "Confirm password" %}</label>
|
|
117
|
+
{% if form.password2.errors %}
|
|
118
|
+
<span class="helper error-text">{{ form.password2.errors.0 }}</span>
|
|
119
|
+
{% endif %}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="large-space"></div>
|
|
123
|
+
|
|
124
|
+
<nav class="right-align">
|
|
125
|
+
<a class="button transparent border round" href="{% url 'index' %}">{% trans "Cancel" %}</a>
|
|
126
|
+
<button type="submit" class="button primary round">{% trans "Sign Up" %}</button>
|
|
127
|
+
</nav>
|
|
40
128
|
|
|
41
|
-
<p style="margin-top:1rem;">
|
|
42
|
-
<button type="submit">{% trans "Sign Up" %}</button>
|
|
43
|
-
<a class="contrast" href="{% url 'index' %}">{% trans "Cancel" %}</a>
|
|
44
|
-
</p>
|
|
45
129
|
</form>
|
|
46
|
-
</
|
|
47
|
-
</
|
|
130
|
+
</article>
|
|
131
|
+
</div>
|
|
48
132
|
</div>
|
|
49
|
-
|
|
50
133
|
{% endblock %}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django import template
|
|
2
|
+
|
|
3
|
+
register = template.Library()
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register.filter
|
|
7
|
+
def split(value, arg):
|
|
8
|
+
return value.split(arg)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register.filter
|
|
12
|
+
def strip(value):
|
|
13
|
+
if isinstance(value, str):
|
|
14
|
+
return value.strip()
|
|
15
|
+
return value
|
sandwitches/urls.py
CHANGED
|
@@ -20,6 +20,7 @@ from django.urls import path, include
|
|
|
20
20
|
from . import views
|
|
21
21
|
from .api import api
|
|
22
22
|
from django.conf.urls.i18n import i18n_patterns
|
|
23
|
+
from .feeds import LatestRecipesFeed # Import the feed class
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
import os
|
|
@@ -32,13 +33,68 @@ urlpatterns = [
|
|
|
32
33
|
path("admin/", admin.site.urls),
|
|
33
34
|
path("api/", api.urls),
|
|
34
35
|
path("media/<path:file_path>", views.media, name="media"),
|
|
36
|
+
path("favorites/", views.favorites, name="favorites"),
|
|
35
37
|
path("", views.index, name="index"),
|
|
38
|
+
path("feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"),
|
|
39
|
+
path(
|
|
40
|
+
"feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"
|
|
41
|
+
), # Add this line
|
|
36
42
|
]
|
|
37
43
|
|
|
38
44
|
urlpatterns += i18n_patterns(
|
|
39
45
|
path("recipes/<slug:slug>/", views.recipe_detail, name="recipe_detail"),
|
|
40
46
|
path("setup/", views.setup, name="setup"),
|
|
41
47
|
path("recipes/<int:pk>/rate/", views.recipe_rate, name="recipe_rate"),
|
|
48
|
+
path("recipes/<int:pk>/favorite/", views.toggle_favorite, name="toggle_favorite"),
|
|
49
|
+
path("dashboard/", views.admin_dashboard, name="admin_dashboard"),
|
|
50
|
+
path("dashboard/recipes/", views.admin_recipe_list, name="admin_recipe_list"),
|
|
51
|
+
path("dashboard/recipes/add/", views.admin_recipe_add, name="admin_recipe_add"),
|
|
52
|
+
path(
|
|
53
|
+
"dashboard/recipes/<int:pk>/edit/",
|
|
54
|
+
views.admin_recipe_edit,
|
|
55
|
+
name="admin_recipe_edit",
|
|
56
|
+
),
|
|
57
|
+
path(
|
|
58
|
+
"dashboard/recipes/<int:pk>/delete/",
|
|
59
|
+
views.admin_recipe_delete,
|
|
60
|
+
name="admin_recipe_delete",
|
|
61
|
+
),
|
|
62
|
+
path(
|
|
63
|
+
"dashboard/recipes/<int:pk>/rotate/",
|
|
64
|
+
views.admin_recipe_rotate,
|
|
65
|
+
name="admin_recipe_rotate",
|
|
66
|
+
),
|
|
67
|
+
path("dashboard/users/", views.admin_user_list, name="admin_user_list"),
|
|
68
|
+
path(
|
|
69
|
+
"dashboard/users/<int:pk>/edit/", views.admin_user_edit, name="admin_user_edit"
|
|
70
|
+
),
|
|
71
|
+
path(
|
|
72
|
+
"dashboard/users/<int:pk>/delete/",
|
|
73
|
+
views.admin_user_delete,
|
|
74
|
+
name="admin_user_delete",
|
|
75
|
+
),
|
|
76
|
+
path("dashboard/tags/", views.admin_tag_list, name="admin_tag_list"),
|
|
77
|
+
path("dashboard/tags/add/", views.admin_tag_add, name="admin_tag_add"),
|
|
78
|
+
path(
|
|
79
|
+
"dashboard/tags/<int:pk>/edit/",
|
|
80
|
+
views.admin_tag_edit,
|
|
81
|
+
name="admin_tag_edit",
|
|
82
|
+
),
|
|
83
|
+
path(
|
|
84
|
+
"dashboard/tags/<int:pk>/delete/",
|
|
85
|
+
views.admin_tag_delete,
|
|
86
|
+
name="admin_tag_delete",
|
|
87
|
+
),
|
|
88
|
+
path("dashboard/tasks/", views.admin_task_list, name="admin_task_list"),
|
|
89
|
+
path(
|
|
90
|
+
"dashboard/tasks/<str:pk>/", views.admin_task_detail, name="admin_task_detail"
|
|
91
|
+
),
|
|
92
|
+
path("dashboard/ratings/", views.admin_rating_list, name="admin_rating_list"),
|
|
93
|
+
path(
|
|
94
|
+
"dashboard/ratings/<int:pk>/delete/",
|
|
95
|
+
views.admin_rating_delete,
|
|
96
|
+
name="admin_rating_delete",
|
|
97
|
+
),
|
|
42
98
|
prefix_default_language=True,
|
|
43
99
|
)
|
|
44
100
|
|
sandwitches/utils.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
# Define a set of common units for better parsing
|
|
4
|
+
COMMON_UNITS = {
|
|
5
|
+
"cup",
|
|
6
|
+
"cups",
|
|
7
|
+
"oz",
|
|
8
|
+
"ounce",
|
|
9
|
+
"ounces",
|
|
10
|
+
"g",
|
|
11
|
+
"gram",
|
|
12
|
+
"grams",
|
|
13
|
+
"kg",
|
|
14
|
+
"kilogram",
|
|
15
|
+
"kilograms",
|
|
16
|
+
"lb",
|
|
17
|
+
"lbs",
|
|
18
|
+
"pound",
|
|
19
|
+
"pounds",
|
|
20
|
+
"ml",
|
|
21
|
+
"milliliter",
|
|
22
|
+
"milliliters",
|
|
23
|
+
"l",
|
|
24
|
+
"liter",
|
|
25
|
+
"liters",
|
|
26
|
+
"tsp",
|
|
27
|
+
"teaspoon",
|
|
28
|
+
"teaspoons",
|
|
29
|
+
"tbsp",
|
|
30
|
+
"tablespoon",
|
|
31
|
+
"tablespoons",
|
|
32
|
+
"pinch",
|
|
33
|
+
"pinches",
|
|
34
|
+
"slice",
|
|
35
|
+
"slices",
|
|
36
|
+
"clove",
|
|
37
|
+
"cloves",
|
|
38
|
+
"large",
|
|
39
|
+
"medium",
|
|
40
|
+
"small",
|
|
41
|
+
"can",
|
|
42
|
+
"cans",
|
|
43
|
+
"package",
|
|
44
|
+
"packages",
|
|
45
|
+
"piece",
|
|
46
|
+
"pieces",
|
|
47
|
+
"dash",
|
|
48
|
+
"dashes",
|
|
49
|
+
"sprig",
|
|
50
|
+
"sprigs",
|
|
51
|
+
"to taste",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_ingredient_line(line):
|
|
56
|
+
"""
|
|
57
|
+
Parses a single ingredient line to extract quantity, unit, and ingredient name.
|
|
58
|
+
This is a heuristic approach and may not cover all possible formats.
|
|
59
|
+
Returns a dictionary with 'quantity', 'unit', 'name', and 'original_line'.
|
|
60
|
+
Quantity will be a float, unit and name strings.
|
|
61
|
+
If parsing fails, returns None for quantity/unit but includes original_line.
|
|
62
|
+
"""
|
|
63
|
+
original_line = line.strip()
|
|
64
|
+
|
|
65
|
+
quantity = None
|
|
66
|
+
unit = None
|
|
67
|
+
name = original_line
|
|
68
|
+
|
|
69
|
+
# Regex to capture quantity at the beginning:
|
|
70
|
+
# 1. Whole number with fraction (e.g., "1 1/2", "2 3/4")
|
|
71
|
+
# 2. Simple fraction (e.g., "1/2", "3/4")
|
|
72
|
+
# 3. Decimal number (e.g., "0.5", "2.0", ".75")
|
|
73
|
+
# 4. Whole number (e.g., "1", "100")
|
|
74
|
+
# The quantity is optional to handle lines like "Salt to taste".
|
|
75
|
+
#
|
|
76
|
+
# This pattern specifically tries to get the quantity part.
|
|
77
|
+
# It covers:
|
|
78
|
+
# - `1 1/2` (whole number followed by space and fraction)
|
|
79
|
+
# - `1/2` (fraction)
|
|
80
|
+
# - `1.5` (decimal)
|
|
81
|
+
# - `1` (integer)
|
|
82
|
+
quantity_match = re.match(
|
|
83
|
+
r"^\s*(\d+\s+\d/\d|\d/\d|\d+\.?\d*|\.\d+)\s*(.*)", original_line
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
remaining_line = original_line
|
|
87
|
+
|
|
88
|
+
if quantity_match:
|
|
89
|
+
quantity_str = quantity_match.group(1)
|
|
90
|
+
remaining_line = quantity_match.group(2).strip()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Handle fractions like "1 1/2" or "1/2"
|
|
94
|
+
if " " in quantity_str and "/" in quantity_str:
|
|
95
|
+
parts = quantity_str.split(" ", 1)
|
|
96
|
+
whole = float(parts[0])
|
|
97
|
+
fraction_parts = parts[1].split("/")
|
|
98
|
+
numerator = float(fraction_parts[0])
|
|
99
|
+
denominator = float(fraction_parts[1])
|
|
100
|
+
quantity = whole + (numerator / denominator)
|
|
101
|
+
elif "/" in quantity_str:
|
|
102
|
+
parts = quantity_str.split("/")
|
|
103
|
+
numerator = float(parts[0])
|
|
104
|
+
denominator = float(parts[1])
|
|
105
|
+
quantity = numerator / denominator
|
|
106
|
+
else:
|
|
107
|
+
quantity = float(quantity_str)
|
|
108
|
+
except ValueError:
|
|
109
|
+
quantity = None # Parsing quantity failed
|
|
110
|
+
|
|
111
|
+
# Now, try to find a unit in the remaining_line
|
|
112
|
+
# We iterate through common units to find the longest match first
|
|
113
|
+
found_unit = None
|
|
114
|
+
for common_unit in sorted(list(COMMON_UNITS), key=len, reverse=True):
|
|
115
|
+
# Check if the unit is at the beginning of the remaining_line, followed by a space or end of string
|
|
116
|
+
# using word boundary to avoid partial matches (e.g., "cup" in "cupholder")
|
|
117
|
+
unit_regex = r"^\s*" + re.escape(common_unit) + r"(\b|\s|$)" # ty:ignore[invalid-argument-type]
|
|
118
|
+
unit_match = re.match(unit_regex, remaining_line, re.IGNORECASE)
|
|
119
|
+
if unit_match:
|
|
120
|
+
found_unit = remaining_line[: unit_match.end(1)].strip()
|
|
121
|
+
remaining_line = remaining_line[unit_match.end(1) :].strip()
|
|
122
|
+
break # Found the unit, stop searching
|
|
123
|
+
|
|
124
|
+
if found_unit:
|
|
125
|
+
unit = found_unit
|
|
126
|
+
|
|
127
|
+
# The rest of the line is the name
|
|
128
|
+
name = (
|
|
129
|
+
remaining_line
|
|
130
|
+
if remaining_line
|
|
131
|
+
else original_line
|
|
132
|
+
if quantity is None and unit is None
|
|
133
|
+
else ""
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# If quantity was not parsed, and no unit was found, then the whole line is the name
|
|
137
|
+
if quantity is None and unit is None:
|
|
138
|
+
name = original_line
|
|
139
|
+
elif name == "" and (quantity is not None or unit is not None):
|
|
140
|
+
# If we parsed a quantity or unit but no name, it means the name was part of the unit search
|
|
141
|
+
# or it was implicitly part of the quantity_match that took everything.
|
|
142
|
+
# This is a fallback to ensure something is in name if it's not a unit.
|
|
143
|
+
if unit and original_line.lower().startswith(
|
|
144
|
+
f"{quantity_str.lower() if quantity_str else ''} {unit.lower()}".strip()
|
|
145
|
+
):
|
|
146
|
+
name_start_index = original_line.lower().find(unit.lower()) + len(unit)
|
|
147
|
+
potential_name = original_line[name_start_index:].strip()
|
|
148
|
+
if potential_name:
|
|
149
|
+
name = potential_name
|
|
150
|
+
elif quantity_match:
|
|
151
|
+
# Fallback if name is empty and quantity was matched, it means quantity was everything
|
|
152
|
+
# or the unit was consumed.
|
|
153
|
+
if not unit: # If no unit, the remaining_line should be the name
|
|
154
|
+
name = remaining_line
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"quantity": quantity,
|
|
158
|
+
"unit": unit,
|
|
159
|
+
"name": name.strip(),
|
|
160
|
+
"original_line": original_line,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def scale_ingredient(parsed_ingredient, current_servings, target_servings):
|
|
165
|
+
"""
|
|
166
|
+
Scales a parsed ingredient based on current and target servings.
|
|
167
|
+
"""
|
|
168
|
+
if parsed_ingredient["quantity"] is None or current_servings == 0:
|
|
169
|
+
# Cannot scale if quantity is unknown or current servings is zero
|
|
170
|
+
return parsed_ingredient
|
|
171
|
+
|
|
172
|
+
scale_factor = target_servings / current_servings
|
|
173
|
+
scaled_quantity = parsed_ingredient["quantity"] * scale_factor
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"quantity": scaled_quantity,
|
|
177
|
+
"unit": parsed_ingredient["unit"],
|
|
178
|
+
"name": parsed_ingredient["name"],
|
|
179
|
+
"original_line": parsed_ingredient[
|
|
180
|
+
"original_line"
|
|
181
|
+
], # Keep original line reference
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def format_scaled_ingredient(scaled_ingredient):
|
|
186
|
+
"""
|
|
187
|
+
Formats a scaled ingredient back into a human-readable string.
|
|
188
|
+
This will try to reconstruct the string based on the parsed components.
|
|
189
|
+
"""
|
|
190
|
+
quantity = scaled_ingredient["quantity"]
|
|
191
|
+
unit = scaled_ingredient["unit"]
|
|
192
|
+
name = scaled_ingredient["name"]
|
|
193
|
+
|
|
194
|
+
if quantity is None:
|
|
195
|
+
return scaled_ingredient[
|
|
196
|
+
"original_line"
|
|
197
|
+
] # Fallback to original if quantity was not parsed
|
|
198
|
+
|
|
199
|
+
# Simple formatting for now. Could add fraction handling later.
|
|
200
|
+
if quantity == int(quantity):
|
|
201
|
+
quantity_str = str(int(quantity))
|
|
202
|
+
else:
|
|
203
|
+
# Format to 2 decimal places, remove trailing zeros, and remove trailing dot if integer
|
|
204
|
+
quantity_str = f"{quantity:.2f}".rstrip("0").rstrip(".")
|
|
205
|
+
if not quantity_str: # Handle cases like 0.0 -> ""
|
|
206
|
+
quantity_str = "0"
|
|
207
|
+
|
|
208
|
+
parts = []
|
|
209
|
+
if quantity_str:
|
|
210
|
+
parts.append(quantity_str)
|
|
211
|
+
|
|
212
|
+
# Check for units that usually don't have a space before them
|
|
213
|
+
if unit:
|
|
214
|
+
if unit.lower() in ["g", "mg"]: # Only g and mg are typically appended directly
|
|
215
|
+
parts[-1] += unit # Append unit directly to the last part (quantity)
|
|
216
|
+
else:
|
|
217
|
+
parts.append(unit)
|
|
218
|
+
|
|
219
|
+
if name:
|
|
220
|
+
parts.append(name)
|
|
221
|
+
|
|
222
|
+
return " ".join(parts).strip()
|