sandwitches 1.5.0__tar.gz
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-1.5.0/PKG-INFO +104 -0
- sandwitches-1.5.0/README.md +81 -0
- sandwitches-1.5.0/pyproject.toml +47 -0
- sandwitches-1.5.0/src/sandwitches/__init__.py +6 -0
- sandwitches-1.5.0/src/sandwitches/admin.py +68 -0
- sandwitches-1.5.0/src/sandwitches/api.py +207 -0
- sandwitches-1.5.0/src/sandwitches/asgi.py +16 -0
- sandwitches-1.5.0/src/sandwitches/feeds.py +23 -0
- sandwitches-1.5.0/src/sandwitches/forms.py +196 -0
- sandwitches-1.5.0/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
- sandwitches-1.5.0/src/sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
- sandwitches-1.5.0/src/sandwitches/migrations/0001_initial.py +328 -0
- sandwitches-1.5.0/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
- sandwitches-1.5.0/src/sandwitches/migrations/0003_setting.py +35 -0
- sandwitches-1.5.0/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
- sandwitches-1.5.0/src/sandwitches/migrations/0005_rating_comment.py +17 -0
- sandwitches-1.5.0/src/sandwitches/migrations/__init__.py +0 -0
- sandwitches-1.5.0/src/sandwitches/models.py +217 -0
- sandwitches-1.5.0/src/sandwitches/settings.py +217 -0
- sandwitches-1.5.0/src/sandwitches/storage.py +114 -0
- sandwitches-1.5.0/src/sandwitches/tasks.py +115 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/admin_base.html +118 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/confirm_delete.html +23 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/dashboard.html +262 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/rating_list.html +38 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/recipe_form.html +184 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/recipe_list.html +64 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/tag_form.html +30 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/tag_list.html +37 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/task_detail.html +91 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/task_list.html +41 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/user_form.html +37 -0
- sandwitches-1.5.0/src/sandwitches/templates/admin/user_list.html +60 -0
- sandwitches-1.5.0/src/sandwitches/templates/base.html +94 -0
- sandwitches-1.5.0/src/sandwitches/templates/base_beer.html +57 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/favorites_search_form.html +85 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/footer.html +14 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/ingredients_scripts.html +50 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/ingredients_section.html +11 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/instructions_section.html +9 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/language_dialog.html +26 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/navbar.html +27 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/rating_section.html +66 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/recipe_header.html +32 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/search_form.html +106 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/search_scripts.html +98 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/side_menu.html +31 -0
- sandwitches-1.5.0/src/sandwitches/templates/components/user_menu.html +10 -0
- sandwitches-1.5.0/src/sandwitches/templates/detail.html +178 -0
- sandwitches-1.5.0/src/sandwitches/templates/favorites.html +42 -0
- sandwitches-1.5.0/src/sandwitches/templates/index.html +42 -0
- sandwitches-1.5.0/src/sandwitches/templates/partials/recipe_list.html +87 -0
- sandwitches-1.5.0/src/sandwitches/templates/recipe_form.html +119 -0
- sandwitches-1.5.0/src/sandwitches/templates/setup.html +55 -0
- sandwitches-1.5.0/src/sandwitches/templates/signup.html +133 -0
- sandwitches-1.5.0/src/sandwitches/templatetags/__init__.py +0 -0
- sandwitches-1.5.0/src/sandwitches/templatetags/custom_filters.py +15 -0
- sandwitches-1.5.0/src/sandwitches/templatetags/markdown_extras.py +17 -0
- sandwitches-1.5.0/src/sandwitches/urls.py +106 -0
- sandwitches-1.5.0/src/sandwitches/utils.py +222 -0
- sandwitches-1.5.0/src/sandwitches/views.py +637 -0
- sandwitches-1.5.0/src/sandwitches/wsgi.py +16 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sandwitches
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Martyn van Dijke
|
|
6
|
+
Author-email: Martyn van Dijke <martijnvdijke600@gmail.com>
|
|
7
|
+
Requires-Dist: django-debug-toolbar>=6.1.0
|
|
8
|
+
Requires-Dist: django-filter>=25.2
|
|
9
|
+
Requires-Dist: django-imagekit>=6.0.0
|
|
10
|
+
Requires-Dist: django-import-export>=4.3.14
|
|
11
|
+
Requires-Dist: django-ninja>=1.5.1
|
|
12
|
+
Requires-Dist: django-simple-history>=3.10.1
|
|
13
|
+
Requires-Dist: django-tasks>=0.10.0
|
|
14
|
+
Requires-Dist: django-solo>=2.3.0
|
|
15
|
+
Requires-Dist: django>=6.0.0
|
|
16
|
+
Requires-Dist: gunicorn>=23.0.0
|
|
17
|
+
Requires-Dist: markdown>=3.10
|
|
18
|
+
Requires-Dist: pillow>=12.0.0
|
|
19
|
+
Requires-Dist: uvicorn>=0.40.0
|
|
20
|
+
Requires-Dist: whitenoise[brotli]>=6.11.0
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="src/static/icons/banner.svg" alt="Sandwitches Banner" width="600px">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<h1 align="center">🥪 Sandwitches</h1>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<strong>Sandwiches so good, they haunt you!</strong>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://github.com/martynvdijke/sandwitches/actions/workflows/ci.yaml">
|
|
36
|
+
<img src="https://github.com/martynvdijke/sandwitches/actions/workflows/ci.yaml/badge.svg" alt="CI Status">
|
|
37
|
+
</a>
|
|
38
|
+
<a href="https://github.com/martynvdijke/sandwitches/blob/main/LICENSE">
|
|
39
|
+
<img src="https://img.shields.io/github/license/martynvdijke/sandwitches" alt="License">
|
|
40
|
+
</a>
|
|
41
|
+
<img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python Version">
|
|
42
|
+
<img src="https://img.shields.io/badge/django-6.0-green.svg" alt="Django Version">
|
|
43
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
44
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
45
|
+
</a>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## ✨ Overview
|
|
51
|
+
|
|
52
|
+
Sandwitches is a modern, recipe management platform built with **Django 6.0**.
|
|
53
|
+
It is made as a hobby project for my girlfriend, who likes to make what I call "fancy" sandwiches (sandwiches that go beyond the Dutch normals), lucky to be me :).
|
|
54
|
+
See wanted to have a way to advertise and share those sandwtiches with the family and so I started coding making it happen, in the hopes of getting more fancy sandwiches.
|
|
55
|
+
|
|
56
|
+
## 📥 Getting Started
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
|
|
60
|
+
* Python 3.12+
|
|
61
|
+
* [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
|
62
|
+
|
|
63
|
+
### Installation
|
|
64
|
+
|
|
65
|
+
1. **Clone the repository**:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/martynvdijke/sandwitches.git
|
|
69
|
+
cd sandwitches
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. **Sync dependencies**:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
3. **Run migrations and collect static files**:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run invoke setup-ci # Sets up environment variables
|
|
82
|
+
uv run src/manage.py migrate
|
|
83
|
+
uv run src/manage.py collectstatic --noinput
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
4. **Start the development server**:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run src/manage.py runserver
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 🧪 Testing & Quality
|
|
93
|
+
|
|
94
|
+
The project maintains high standards with over **80+ automated tests**.
|
|
95
|
+
|
|
96
|
+
* **Run tests**: `uv run invoke tests`
|
|
97
|
+
* **Linting**: `uv run invoke linting`
|
|
98
|
+
* **Type checking**: `uv run invoke typecheck`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<p align="center">
|
|
103
|
+
Made with ❤️ for sandwich enthusiasts.
|
|
104
|
+
</p>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="src/static/icons/banner.svg" alt="Sandwitches Banner" width="600px">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">🥪 Sandwitches</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Sandwiches so good, they haunt you!</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/martynvdijke/sandwitches/actions/workflows/ci.yaml">
|
|
13
|
+
<img src="https://github.com/martynvdijke/sandwitches/actions/workflows/ci.yaml/badge.svg" alt="CI Status">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://github.com/martynvdijke/sandwitches/blob/main/LICENSE">
|
|
16
|
+
<img src="https://img.shields.io/github/license/martynvdijke/sandwitches" alt="License">
|
|
17
|
+
</a>
|
|
18
|
+
<img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python Version">
|
|
19
|
+
<img src="https://img.shields.io/badge/django-6.0-green.svg" alt="Django Version">
|
|
20
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
21
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## ✨ Overview
|
|
28
|
+
|
|
29
|
+
Sandwitches is a modern, recipe management platform built with **Django 6.0**.
|
|
30
|
+
It is made as a hobby project for my girlfriend, who likes to make what I call "fancy" sandwiches (sandwiches that go beyond the Dutch normals), lucky to be me :).
|
|
31
|
+
See wanted to have a way to advertise and share those sandwtiches with the family and so I started coding making it happen, in the hopes of getting more fancy sandwiches.
|
|
32
|
+
|
|
33
|
+
## 📥 Getting Started
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
* Python 3.12+
|
|
38
|
+
* [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
41
|
+
|
|
42
|
+
1. **Clone the repository**:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/martynvdijke/sandwitches.git
|
|
46
|
+
cd sandwitches
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. **Sync dependencies**:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. **Run migrations and collect static files**:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv run invoke setup-ci # Sets up environment variables
|
|
59
|
+
uv run src/manage.py migrate
|
|
60
|
+
uv run src/manage.py collectstatic --noinput
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
4. **Start the development server**:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run src/manage.py runserver
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 🧪 Testing & Quality
|
|
70
|
+
|
|
71
|
+
The project maintains high standards with over **80+ automated tests**.
|
|
72
|
+
|
|
73
|
+
* **Run tests**: `uv run invoke tests`
|
|
74
|
+
* **Linting**: `uv run invoke linting`
|
|
75
|
+
* **Type checking**: `uv run invoke typecheck`
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<p align="center">
|
|
80
|
+
Made with ❤️ for sandwich enthusiasts.
|
|
81
|
+
</p>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sandwitches"
|
|
3
|
+
version = "1.5.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Martyn van Dijke", email = "martijnvdijke600@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
"django-debug-toolbar>=6.1.0",
|
|
14
|
+
"django-filter>=25.2",
|
|
15
|
+
"django-imagekit>=6.0.0",
|
|
16
|
+
"django-import-export>=4.3.14",
|
|
17
|
+
"django-ninja>=1.5.1",
|
|
18
|
+
"django-simple-history>=3.10.1",
|
|
19
|
+
"django-tasks>=0.10.0",
|
|
20
|
+
"django-solo>=2.3.0",
|
|
21
|
+
"django>=6.0.0",
|
|
22
|
+
"gunicorn>=23.0.0",
|
|
23
|
+
"markdown>=3.10",
|
|
24
|
+
"pillow>=12.0.0",
|
|
25
|
+
"uvicorn>=0.40.0",
|
|
26
|
+
"whitenoise[brotli]>=6.11.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.8.19,<0.9.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"invoke>=2.2.1",
|
|
37
|
+
"pytest-django>=4.11.1",
|
|
38
|
+
"pytest>=8.4.2",
|
|
39
|
+
"ruff>=0.14.3",
|
|
40
|
+
"ty>=0.0.1a26",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.uv]
|
|
44
|
+
package = true
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
DJANGO_SETTINGS_MODULE = "sandwitches.settings"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from django.contrib.auth.admin import UserAdmin
|
|
4
|
+
from .models import Recipe, Tag, Rating, Setting
|
|
5
|
+
from django.utils.html import format_html
|
|
6
|
+
from import_export import resources
|
|
7
|
+
from import_export.admin import ImportExportModelAdmin
|
|
8
|
+
from solo.admin import SingletonModelAdmin
|
|
9
|
+
from .forms import SettingForm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RecipeResource(resources.ModelResource):
|
|
13
|
+
class Meta:
|
|
14
|
+
model = Recipe
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TagResource(resources.ModelResource):
|
|
18
|
+
class Meta:
|
|
19
|
+
model = Tag
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RatingResource(resources.ModelResource):
|
|
23
|
+
class Meta:
|
|
24
|
+
model = Rating
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
User = get_user_model()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@admin.register(Setting)
|
|
31
|
+
class SettingAdmin(SingletonModelAdmin):
|
|
32
|
+
form = SettingForm
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@admin.register(User)
|
|
36
|
+
class CustomUserAdmin(UserAdmin):
|
|
37
|
+
fieldsets = UserAdmin.fieldsets + (
|
|
38
|
+
(None, {"fields": ("avatar", "bio", "language", "favorites")}),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@admin.register(Recipe)
|
|
43
|
+
class RecipeAdmin(ImportExportModelAdmin):
|
|
44
|
+
resource_classes = [RecipeResource]
|
|
45
|
+
list_display = ("title", "uploaded_by", "created_at", "show_url")
|
|
46
|
+
readonly_fields = ("created_at", "updated_at")
|
|
47
|
+
|
|
48
|
+
def save_model(self, request, obj, form, change):
|
|
49
|
+
# set uploaded_by automatically when creating in admin
|
|
50
|
+
if not change and not obj.uploaded_by:
|
|
51
|
+
obj.uploaded_by = request.user
|
|
52
|
+
super().save_model(request, obj, form, change)
|
|
53
|
+
|
|
54
|
+
def show_url(self, obj):
|
|
55
|
+
url = obj.get_absolute_url()
|
|
56
|
+
return format_html("<a href='{url}'>{url}</a>", url=url)
|
|
57
|
+
|
|
58
|
+
show_url.short_description = "Recipe Link" # ty:ignore[unresolved-attribute]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@admin.register(Tag)
|
|
62
|
+
class TagAdmin(ImportExportModelAdmin):
|
|
63
|
+
resource_classes = [TagResource]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@admin.register(Rating)
|
|
67
|
+
class RatingAdmin(ImportExportModelAdmin):
|
|
68
|
+
resource_classes = [RatingResource]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from ninja import NinjaAPI
|
|
3
|
+
from .models import Recipe, Tag, Setting, Rating
|
|
4
|
+
from django.contrib.auth import get_user_model
|
|
5
|
+
from .utils import (
|
|
6
|
+
parse_ingredient_line,
|
|
7
|
+
scale_ingredient,
|
|
8
|
+
format_scaled_ingredient,
|
|
9
|
+
) # Import utility functions
|
|
10
|
+
|
|
11
|
+
from ninja import ModelSchema
|
|
12
|
+
from ninja import Schema
|
|
13
|
+
from django.shortcuts import get_object_or_404
|
|
14
|
+
from datetime import date
|
|
15
|
+
import random
|
|
16
|
+
from typing import List, Optional # Import typing hints
|
|
17
|
+
|
|
18
|
+
from ninja.security import django_auth
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
|
|
22
|
+
# Get the custom User model
|
|
23
|
+
User = get_user_model()
|
|
24
|
+
|
|
25
|
+
api = NinjaAPI(version=__version__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserPublicSchema(ModelSchema):
|
|
29
|
+
class Meta:
|
|
30
|
+
model = User
|
|
31
|
+
fields = ["username", "first_name", "last_name", "avatar"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RecipeSchema(ModelSchema):
|
|
35
|
+
favorited_by: List[UserPublicSchema] = []
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
model = Recipe
|
|
39
|
+
fields = "__all__"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TagSchema(ModelSchema):
|
|
43
|
+
class Meta:
|
|
44
|
+
model = Tag
|
|
45
|
+
fields = "__all__"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserSchema(ModelSchema):
|
|
49
|
+
class Meta:
|
|
50
|
+
model = User
|
|
51
|
+
exclude = ["password", "last_login", "user_permissions"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SettingSchema(ModelSchema):
|
|
55
|
+
class Meta:
|
|
56
|
+
model = Setting
|
|
57
|
+
fields = "__all__"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RatingSchema(ModelSchema):
|
|
61
|
+
user: UserPublicSchema
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
model = Rating
|
|
65
|
+
fields = "__all__"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Error(Schema):
|
|
69
|
+
message: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RatingResponseSchema(Schema):
|
|
73
|
+
average: float
|
|
74
|
+
count: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ScaledIngredient(Schema): # New Schema for scaled ingredients
|
|
78
|
+
original_line: str
|
|
79
|
+
scaled_line: str
|
|
80
|
+
quantity: Optional[float]
|
|
81
|
+
unit: Optional[str]
|
|
82
|
+
name: Optional[str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@api.get("ping")
|
|
86
|
+
def ping(request):
|
|
87
|
+
return {"status": "ok", "message": "pong"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@api.get("v1/settings", response=SettingSchema)
|
|
91
|
+
def get_settings(request):
|
|
92
|
+
return Setting.objects.get() # ty:ignore[unresolved-attribute]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@api.post("v1/settings", auth=django_auth, response={200: SettingSchema, 403: Error})
|
|
96
|
+
def update_settings(request, payload: SettingSchema):
|
|
97
|
+
if not request.user.is_staff:
|
|
98
|
+
return 403, {"message": "You are not authorized to perform this action"}
|
|
99
|
+
settings = Setting.objects.get() # ty:ignore[unresolved-attribute]
|
|
100
|
+
for attr, value in payload.dict().items():
|
|
101
|
+
setattr(settings, attr, value)
|
|
102
|
+
settings.save()
|
|
103
|
+
return settings
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@api.get("v1/me", response={200: UserSchema, 403: Error})
|
|
107
|
+
def me(request):
|
|
108
|
+
if not request.user.is_authenticated:
|
|
109
|
+
return 403, {"message": "Please sign in first"}
|
|
110
|
+
return request.user
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@api.get("v1/users", auth=django_auth, response=list[UserSchema])
|
|
114
|
+
def users(request):
|
|
115
|
+
return User.objects.all()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@api.get("v1/recipes", response=list[RecipeSchema])
|
|
119
|
+
def get_recipes(request):
|
|
120
|
+
return Recipe.objects.all().prefetch_related("favorited_by") # ty:ignore[unresolved-attribute]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@api.get("v1/recipes/{recipe_id}", response=RecipeSchema)
|
|
124
|
+
def get_recipe(request, recipe_id: int):
|
|
125
|
+
recipe = get_object_or_404(
|
|
126
|
+
Recipe.objects.prefetch_related("favorited_by"), # ty:ignore[unresolved-attribute]
|
|
127
|
+
id=recipe_id,
|
|
128
|
+
)
|
|
129
|
+
return recipe
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@api.get("v1/recipes/{recipe_id}/scale-ingredients", response=List[ScaledIngredient])
|
|
133
|
+
def scale_recipe_ingredients(request, recipe_id: int, target_servings: int):
|
|
134
|
+
recipe = get_object_or_404(Recipe, id=recipe_id)
|
|
135
|
+
|
|
136
|
+
# Ensure target_servings is at least 1
|
|
137
|
+
target_servings = max(1, target_servings)
|
|
138
|
+
|
|
139
|
+
current_servings = recipe.servings
|
|
140
|
+
if not current_servings or current_servings <= 0:
|
|
141
|
+
current_servings = 1
|
|
142
|
+
|
|
143
|
+
ingredient_lines = [
|
|
144
|
+
line.strip() for line in (recipe.ingredients or "").split("\n") if line.strip()
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
scaled_ingredients_output = []
|
|
148
|
+
for line in ingredient_lines:
|
|
149
|
+
try:
|
|
150
|
+
parsed = parse_ingredient_line(line)
|
|
151
|
+
scaled = scale_ingredient(parsed, current_servings, target_servings)
|
|
152
|
+
formatted_line = format_scaled_ingredient(scaled)
|
|
153
|
+
|
|
154
|
+
scaled_ingredients_output.append(
|
|
155
|
+
ScaledIngredient(
|
|
156
|
+
original_line=line,
|
|
157
|
+
scaled_line=formatted_line,
|
|
158
|
+
quantity=scaled.get("quantity"),
|
|
159
|
+
unit=scaled.get("unit"),
|
|
160
|
+
name=scaled.get("name"),
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# Fallback for lines that fail to parse/scale
|
|
165
|
+
scaled_ingredients_output.append(
|
|
166
|
+
ScaledIngredient(
|
|
167
|
+
original_line=line,
|
|
168
|
+
scaled_line=line,
|
|
169
|
+
quantity=None,
|
|
170
|
+
unit=None,
|
|
171
|
+
name=line,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
logging.warning(f"Failed to scale ingredient line '{line}': {e}")
|
|
175
|
+
|
|
176
|
+
return scaled_ingredients_output
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@api.get("v1/recipe-of-the-day", response=RecipeSchema)
|
|
180
|
+
def get_recipe_of_the_day(request):
|
|
181
|
+
recipes = list(Recipe.objects.all().prefetch_related("favorited_by")) # ty:ignore[unresolved-attribute]
|
|
182
|
+
if not recipes:
|
|
183
|
+
return None
|
|
184
|
+
today = date.today()
|
|
185
|
+
random.seed(today.toordinal())
|
|
186
|
+
recipe = random.choice(recipes)
|
|
187
|
+
return recipe
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@api.get("v1/recipes/{recipe_id}/rating", response=RatingResponseSchema)
|
|
191
|
+
def get_recipe_rating(request, recipe_id: int):
|
|
192
|
+
recipe = get_object_or_404(Recipe, id=recipe_id)
|
|
193
|
+
return {
|
|
194
|
+
"average": recipe.average_rating(),
|
|
195
|
+
"count": recipe.rating_count(),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@api.get("v1/tags", response=list[TagSchema])
|
|
200
|
+
def get_tags(request):
|
|
201
|
+
return Tag.objects.all() # ty:ignore[unresolved-attribute]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@api.get("v1/tags/{tag_id}", response=TagSchema)
|
|
205
|
+
def get_tag(request, tag_id: int):
|
|
206
|
+
tag = get_object_or_404(Tag, id=tag_id)
|
|
207
|
+
return tag
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ASGI config for sandwitches project.
|
|
3
|
+
|
|
4
|
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
5
|
+
|
|
6
|
+
For more information on this file, see
|
|
7
|
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from django.core.asgi import get_asgi_application
|
|
13
|
+
|
|
14
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandwitches.settings")
|
|
15
|
+
|
|
16
|
+
application = get_asgi_application()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from django.contrib.syndication.views import Feed
|
|
2
|
+
from django.urls import reverse_lazy
|
|
3
|
+
from .models import Recipe
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LatestRecipesFeed(Feed):
|
|
7
|
+
title = "Sandwitches - Latest Recipes"
|
|
8
|
+
link = reverse_lazy(
|
|
9
|
+
"index"
|
|
10
|
+
) # This should point to the homepage or a list of recipes
|
|
11
|
+
description = "Updates on the newest recipes added to Sandwitches."
|
|
12
|
+
|
|
13
|
+
def items(self):
|
|
14
|
+
return Recipe.objects.order_by("-created_at")[:5] # ty:ignore[unresolved-attribute]
|
|
15
|
+
|
|
16
|
+
def item_title(self, item):
|
|
17
|
+
return item.title
|
|
18
|
+
|
|
19
|
+
def item_description(self, item):
|
|
20
|
+
return item.description
|
|
21
|
+
|
|
22
|
+
def item_link(self, item):
|
|
23
|
+
return item.get_absolute_url()
|