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.
Files changed (65) hide show
  1. sandwitches/__init__.py +6 -0
  2. sandwitches/admin.py +69 -0
  3. sandwitches/api.py +207 -0
  4. sandwitches/asgi.py +16 -0
  5. sandwitches/feeds.py +23 -0
  6. sandwitches/forms.py +196 -0
  7. sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  8. sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
  9. sandwitches/migrations/0001_initial.py +328 -0
  10. sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
  11. sandwitches/migrations/0003_setting.py +35 -0
  12. sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
  13. sandwitches/migrations/0005_rating_comment.py +17 -0
  14. sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
  15. sandwitches/migrations/__init__.py +0 -0
  16. sandwitches/models.py +218 -0
  17. sandwitches/settings.py +220 -0
  18. sandwitches/storage.py +114 -0
  19. sandwitches/tasks.py +115 -0
  20. sandwitches/templates/admin/admin_base.html +118 -0
  21. sandwitches/templates/admin/confirm_delete.html +23 -0
  22. sandwitches/templates/admin/dashboard.html +262 -0
  23. sandwitches/templates/admin/rating_list.html +38 -0
  24. sandwitches/templates/admin/recipe_form.html +184 -0
  25. sandwitches/templates/admin/recipe_list.html +64 -0
  26. sandwitches/templates/admin/tag_form.html +30 -0
  27. sandwitches/templates/admin/tag_list.html +37 -0
  28. sandwitches/templates/admin/task_detail.html +91 -0
  29. sandwitches/templates/admin/task_list.html +41 -0
  30. sandwitches/templates/admin/user_form.html +37 -0
  31. sandwitches/templates/admin/user_list.html +60 -0
  32. sandwitches/templates/base.html +94 -0
  33. sandwitches/templates/base_beer.html +57 -0
  34. sandwitches/templates/components/carousel_scripts.html +59 -0
  35. sandwitches/templates/components/favorites_search_form.html +85 -0
  36. sandwitches/templates/components/footer.html +14 -0
  37. sandwitches/templates/components/ingredients_scripts.html +50 -0
  38. sandwitches/templates/components/ingredients_section.html +11 -0
  39. sandwitches/templates/components/instructions_section.html +9 -0
  40. sandwitches/templates/components/language_dialog.html +26 -0
  41. sandwitches/templates/components/navbar.html +27 -0
  42. sandwitches/templates/components/rating_section.html +66 -0
  43. sandwitches/templates/components/recipe_header.html +32 -0
  44. sandwitches/templates/components/search_form.html +106 -0
  45. sandwitches/templates/components/search_scripts.html +98 -0
  46. sandwitches/templates/components/side_menu.html +35 -0
  47. sandwitches/templates/components/user_menu.html +10 -0
  48. sandwitches/templates/detail.html +178 -0
  49. sandwitches/templates/favorites.html +42 -0
  50. sandwitches/templates/index.html +76 -0
  51. sandwitches/templates/login.html +57 -0
  52. sandwitches/templates/partials/recipe_list.html +87 -0
  53. sandwitches/templates/recipe_form.html +119 -0
  54. sandwitches/templates/setup.html +105 -0
  55. sandwitches/templates/signup.html +133 -0
  56. sandwitches/templatetags/__init__.py +0 -0
  57. sandwitches/templatetags/custom_filters.py +15 -0
  58. sandwitches/templatetags/markdown_extras.py +17 -0
  59. sandwitches/urls.py +109 -0
  60. sandwitches/utils.py +222 -0
  61. sandwitches/views.py +647 -0
  62. sandwitches/wsgi.py +16 -0
  63. sandwitches-2.2.0.dist-info/METADATA +104 -0
  64. sandwitches-2.2.0.dist-info/RECORD +65 -0
  65. 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()