sandwitches 2.2.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 +69 -0
- sandwitches/api.py +207 -0
- sandwitches/asgi.py +16 -0
- sandwitches/feeds.py +23 -0
- sandwitches/forms.py +196 -0
- sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
- sandwitches/migrations/0001_initial.py +328 -0
- 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/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
- sandwitches/migrations/__init__.py +0 -0
- sandwitches/models.py +218 -0
- sandwitches/settings.py +220 -0
- sandwitches/storage.py +114 -0
- sandwitches/tasks.py +115 -0
- 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 +94 -0
- sandwitches/templates/base_beer.html +57 -0
- sandwitches/templates/components/carousel_scripts.html +59 -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 +35 -0
- sandwitches/templates/components/user_menu.html +10 -0
- sandwitches/templates/detail.html +178 -0
- sandwitches/templates/favorites.html +42 -0
- sandwitches/templates/index.html +76 -0
- sandwitches/templates/login.html +57 -0
- sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches/templates/recipe_form.html +119 -0
- sandwitches/templates/setup.html +105 -0
- sandwitches/templates/signup.html +133 -0
- sandwitches/templatetags/__init__.py +0 -0
- sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches/templatetags/markdown_extras.py +17 -0
- sandwitches/urls.py +109 -0
- sandwitches/utils.py +222 -0
- sandwitches/views.py +647 -0
- sandwitches/wsgi.py +16 -0
- sandwitches-2.2.0.dist-info/METADATA +104 -0
- sandwitches-2.2.0.dist-info/RECORD +65 -0
- sandwitches-2.2.0.dist-info/WHEEL +4 -0
sandwitches/urls.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL configuration for sandwitches project.
|
|
3
|
+
|
|
4
|
+
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
5
|
+
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
|
6
|
+
Examples:
|
|
7
|
+
Function views
|
|
8
|
+
1. Add an import: from my_app import views
|
|
9
|
+
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
10
|
+
Class-based views
|
|
11
|
+
1. Add an import: from other_app.views import Home
|
|
12
|
+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
13
|
+
Including another URLconf
|
|
14
|
+
1. Import the include() function: from django.urls import include, path
|
|
15
|
+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from django.contrib import admin
|
|
19
|
+
from django.urls import path, include
|
|
20
|
+
from . import views
|
|
21
|
+
from .api import api
|
|
22
|
+
from django.conf.urls.i18n import i18n_patterns
|
|
23
|
+
from .feeds import LatestRecipesFeed # Import the feed class
|
|
24
|
+
from django.contrib.auth.views import LogoutView # Import LogoutView
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
urlpatterns = [
|
|
32
|
+
path("i18n/", include("django.conf.urls.i18n")),
|
|
33
|
+
path("signup/", views.signup, name="signup"),
|
|
34
|
+
path("login/", views.CustomLoginView.as_view(), name="login"),
|
|
35
|
+
path("logout/", LogoutView.as_view(next_page="index"), name="logout"),
|
|
36
|
+
path("admin/", admin.site.urls),
|
|
37
|
+
path("api/", api.urls),
|
|
38
|
+
path("media/<path:file_path>", views.media, name="media"),
|
|
39
|
+
path("favorites/", views.favorites, name="favorites"),
|
|
40
|
+
path("", views.index, name="index"),
|
|
41
|
+
path("feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"),
|
|
42
|
+
path(
|
|
43
|
+
"feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"
|
|
44
|
+
), # Add this line
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
urlpatterns += i18n_patterns(
|
|
48
|
+
path("recipes/<slug:slug>/", views.recipe_detail, name="recipe_detail"),
|
|
49
|
+
path("setup/", views.setup, name="setup"),
|
|
50
|
+
path("recipes/<int:pk>/rate/", views.recipe_rate, name="recipe_rate"),
|
|
51
|
+
path("recipes/<int:pk>/favorite/", views.toggle_favorite, name="toggle_favorite"),
|
|
52
|
+
path("dashboard/", views.admin_dashboard, name="admin_dashboard"),
|
|
53
|
+
path("dashboard/recipes/", views.admin_recipe_list, name="admin_recipe_list"),
|
|
54
|
+
path("dashboard/recipes/add/", views.admin_recipe_add, name="admin_recipe_add"),
|
|
55
|
+
path(
|
|
56
|
+
"dashboard/recipes/<int:pk>/edit/",
|
|
57
|
+
views.admin_recipe_edit,
|
|
58
|
+
name="admin_recipe_edit",
|
|
59
|
+
),
|
|
60
|
+
path(
|
|
61
|
+
"dashboard/recipes/<int:pk>/delete/",
|
|
62
|
+
views.admin_recipe_delete,
|
|
63
|
+
name="admin_recipe_delete",
|
|
64
|
+
),
|
|
65
|
+
path(
|
|
66
|
+
"dashboard/recipes/<int:pk>/rotate/",
|
|
67
|
+
views.admin_recipe_rotate,
|
|
68
|
+
name="admin_recipe_rotate",
|
|
69
|
+
),
|
|
70
|
+
path("dashboard/users/", views.admin_user_list, name="admin_user_list"),
|
|
71
|
+
path(
|
|
72
|
+
"dashboard/users/<int:pk>/edit/", views.admin_user_edit, name="admin_user_edit"
|
|
73
|
+
),
|
|
74
|
+
path(
|
|
75
|
+
"dashboard/users/<int:pk>/delete/",
|
|
76
|
+
views.admin_user_delete,
|
|
77
|
+
name="admin_user_delete",
|
|
78
|
+
),
|
|
79
|
+
path("dashboard/tags/", views.admin_tag_list, name="admin_tag_list"),
|
|
80
|
+
path("dashboard/tags/add/", views.admin_tag_add, name="admin_tag_add"),
|
|
81
|
+
path(
|
|
82
|
+
"dashboard/tags/<int:pk>/edit/",
|
|
83
|
+
views.admin_tag_edit,
|
|
84
|
+
name="admin_tag_edit",
|
|
85
|
+
),
|
|
86
|
+
path(
|
|
87
|
+
"dashboard/tags/<int:pk>/delete/",
|
|
88
|
+
views.admin_tag_delete,
|
|
89
|
+
name="admin_tag_delete",
|
|
90
|
+
),
|
|
91
|
+
path("dashboard/tasks/", views.admin_task_list, name="admin_task_list"),
|
|
92
|
+
path(
|
|
93
|
+
"dashboard/tasks/<str:pk>/", views.admin_task_detail, name="admin_task_detail"
|
|
94
|
+
),
|
|
95
|
+
path("dashboard/ratings/", views.admin_rating_list, name="admin_rating_list"),
|
|
96
|
+
path(
|
|
97
|
+
"dashboard/ratings/<int:pk>/delete/",
|
|
98
|
+
views.admin_rating_delete,
|
|
99
|
+
name="admin_rating_delete",
|
|
100
|
+
),
|
|
101
|
+
prefix_default_language=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if "test" not in sys.argv or "PYTEST_VERSION" in os.environ:
|
|
105
|
+
from debug_toolbar.toolbar import debug_toolbar_urls
|
|
106
|
+
|
|
107
|
+
urlpatterns = [
|
|
108
|
+
*urlpatterns,
|
|
109
|
+
] + debug_toolbar_urls()
|
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()
|