sandwitches 1.0.3__py3-none-any.whl → 1.1.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/api.py +65 -0
- sandwitches/forms.py +58 -29
- sandwitches/migrations/0003_rating.py +57 -0
- sandwitches/models.py +27 -0
- sandwitches/settings.py +7 -1
- sandwitches/tasks.py +16 -0
- sandwitches/templates/base_pico.html +2 -0
- sandwitches/templates/detail.html +60 -0
- sandwitches/templates/signup.html +50 -0
- sandwitches/urls.py +5 -0
- sandwitches/views.py +66 -3
- {sandwitches-1.0.3.dist-info → sandwitches-1.1.0.dist-info}/METADATA +3 -3
- {sandwitches-1.0.3.dist-info → sandwitches-1.1.0.dist-info}/RECORD +14 -10
- {sandwitches-1.0.3.dist-info → sandwitches-1.1.0.dist-info}/WHEEL +0 -0
sandwitches/api.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from ninja import NinjaAPI
|
|
2
|
+
from .models import Recipe
|
|
3
|
+
|
|
4
|
+
from ninja import ModelSchema
|
|
5
|
+
from ninja import Schema
|
|
6
|
+
from django.contrib.auth.models import User
|
|
7
|
+
from django.shortcuts import get_object_or_404
|
|
8
|
+
from datetime import date
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
api = NinjaAPI()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RecipeSchema(ModelSchema):
|
|
16
|
+
class Meta:
|
|
17
|
+
model = Recipe
|
|
18
|
+
fields = "__all__"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UserSchema(ModelSchema):
|
|
22
|
+
class Meta:
|
|
23
|
+
model = User
|
|
24
|
+
exclude = ["password", "last_login", "user_permissions"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Error(Schema):
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@api.get("v1/me", response={200: UserSchema, 403: Error})
|
|
32
|
+
def me(request):
|
|
33
|
+
if not request.user.is_authenticated:
|
|
34
|
+
return 403, {"message": "Please sign in first"}
|
|
35
|
+
return request.user
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# TODO: enable recipe creation via API
|
|
39
|
+
# @api.post("v1/recipe", auth=django_auth, response=RecipeSchema)
|
|
40
|
+
# def create_recipe(request, payload: RecipeSchema):
|
|
41
|
+
# recipe = Recipe.objects.create(**payload.dict())
|
|
42
|
+
# return recipe
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@api.get("v1/recipes", response=list[RecipeSchema])
|
|
46
|
+
def get_recipes(request):
|
|
47
|
+
recipes = Recipe.objects.all()
|
|
48
|
+
return recipes
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@api.get("v1/recipes/{recipe_id}", response=RecipeSchema)
|
|
52
|
+
def get_recipe(request, recipe_id: int):
|
|
53
|
+
recipe = get_object_or_404(Recipe, id=recipe_id)
|
|
54
|
+
return recipe
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@api.get("v1/recipe-of-the-day", response=RecipeSchema)
|
|
58
|
+
def get_recipe_of_the_day(request):
|
|
59
|
+
recipes = list(Recipe.objects.all())
|
|
60
|
+
if not recipes:
|
|
61
|
+
return None
|
|
62
|
+
today = date.today()
|
|
63
|
+
random.seed(today.toordinal())
|
|
64
|
+
recipe = random.choice(recipes)
|
|
65
|
+
return recipe
|
sandwitches/forms.py
CHANGED
|
@@ -1,46 +1,65 @@
|
|
|
1
1
|
from django import forms
|
|
2
2
|
from django.contrib.auth import get_user_model
|
|
3
|
+
from django.contrib.auth.forms import UserCreationForm
|
|
3
4
|
from .models import Recipe
|
|
4
5
|
|
|
5
6
|
User = get_user_model()
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
email = forms.EmailField(required=False, label="Email (optional)")
|
|
11
|
-
password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
|
|
12
|
-
password2 = forms.CharField(label="Confirm password", widget=forms.PasswordInput)
|
|
13
|
-
first_name = forms.CharField(max_length=30, required=False, label="First name")
|
|
14
|
-
last_name = forms.CharField(max_length=150, required=False, label="Last name")
|
|
9
|
+
class BaseUserFormMixin:
|
|
10
|
+
"""Mixin to handle common password validation and user field processing."""
|
|
15
11
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
raise forms.ValidationError("A user with that username already exists.")
|
|
20
|
-
return username
|
|
21
|
-
|
|
22
|
-
def clean(self):
|
|
23
|
-
cleaned = super().clean()
|
|
24
|
-
p1 = cleaned.get("password1")
|
|
25
|
-
p2 = cleaned.get("password2")
|
|
12
|
+
def clean_passwords(self, cleaned_data):
|
|
13
|
+
p1 = cleaned_data.get("password1")
|
|
14
|
+
p2 = cleaned_data.get("password2")
|
|
26
15
|
if p1 and p2 and p1 != p2:
|
|
27
16
|
raise forms.ValidationError("Passwords do not match.")
|
|
28
|
-
return
|
|
17
|
+
return cleaned_data
|
|
18
|
+
|
|
19
|
+
def _set_user_attributes(self, user, data):
|
|
20
|
+
"""Helper to apply optional name fields."""
|
|
21
|
+
user.first_name = data.get("first_name", "")
|
|
22
|
+
user.last_name = data.get("last_name", "")
|
|
23
|
+
user.save()
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdminSetupForm(forms.ModelForm, BaseUserFormMixin):
|
|
28
|
+
password1 = forms.CharField(widget=forms.PasswordInput, label="Password")
|
|
29
|
+
password2 = forms.CharField(widget=forms.PasswordInput, label="Confirm Password")
|
|
30
|
+
|
|
31
|
+
class Meta:
|
|
32
|
+
model = User
|
|
33
|
+
fields = ("username", "first_name", "last_name", "email")
|
|
34
|
+
|
|
35
|
+
def clean(self):
|
|
36
|
+
cleaned_data = super().clean()
|
|
37
|
+
return self.clean_passwords(cleaned_data)
|
|
29
38
|
|
|
30
|
-
def save(self):
|
|
39
|
+
def save(self, commit=True):
|
|
31
40
|
data = self.cleaned_data
|
|
32
41
|
user = User.objects.create_superuser(
|
|
33
|
-
username=data["username"],
|
|
34
|
-
email=data.get("email") or "",
|
|
35
|
-
password=data["password1"],
|
|
42
|
+
username=data["username"], email=data["email"], password=data["password1"]
|
|
36
43
|
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
return self._set_user_attributes(user, data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UserSignupForm(UserCreationForm, BaseUserFormMixin):
|
|
48
|
+
"""Refactored Regular User Form inheriting from Django's UserCreationForm"""
|
|
49
|
+
|
|
50
|
+
class Meta(UserCreationForm.Meta):
|
|
51
|
+
model = User
|
|
52
|
+
fields = ("username", "first_name", "last_name", "email")
|
|
53
|
+
|
|
54
|
+
def clean(self):
|
|
55
|
+
return super().clean()
|
|
56
|
+
|
|
57
|
+
def save(self, commit=True):
|
|
58
|
+
user = super().save(commit=False)
|
|
59
|
+
user.is_superuser = False
|
|
60
|
+
user.is_staff = False
|
|
61
|
+
if commit:
|
|
62
|
+
user.save()
|
|
44
63
|
return user
|
|
45
64
|
|
|
46
65
|
|
|
@@ -58,3 +77,13 @@ class RecipeForm(forms.ModelForm):
|
|
|
58
77
|
widgets = {
|
|
59
78
|
"tags": forms.TextInput(attrs={"placeholder": "tag1,tag2"}),
|
|
60
79
|
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RatingForm(forms.Form):
|
|
83
|
+
"""Simple form for rating recipes (1-5)."""
|
|
84
|
+
|
|
85
|
+
score = forms.ChoiceField(
|
|
86
|
+
choices=[(str(i), str(i)) for i in range(1, 6)],
|
|
87
|
+
widget=forms.RadioSelect,
|
|
88
|
+
label="Your rating",
|
|
89
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Generated by Django 6.0 on 2025-12-21 21:17
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("sandwitches", "0002_historicalrecipe"),
|
|
11
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="Rating",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.BigAutoField(
|
|
21
|
+
auto_created=True,
|
|
22
|
+
primary_key=True,
|
|
23
|
+
serialize=False,
|
|
24
|
+
verbose_name="ID",
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"score",
|
|
29
|
+
models.PositiveSmallIntegerField(
|
|
30
|
+
choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
34
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
35
|
+
(
|
|
36
|
+
"recipe",
|
|
37
|
+
models.ForeignKey(
|
|
38
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
39
|
+
related_name="ratings",
|
|
40
|
+
to="sandwitches.recipe",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"user",
|
|
45
|
+
models.ForeignKey(
|
|
46
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
47
|
+
related_name="ratings",
|
|
48
|
+
to=settings.AUTH_USER_MODEL,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
options={
|
|
53
|
+
"ordering": ("-updated_at",),
|
|
54
|
+
"unique_together": {("recipe", "user")},
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
]
|
sandwitches/models.py
CHANGED
|
@@ -3,9 +3,13 @@ from django.urls import reverse
|
|
|
3
3
|
from django.utils.text import slugify
|
|
4
4
|
from .storage import HashedFilenameStorage
|
|
5
5
|
from simple_history.models import HistoricalRecords
|
|
6
|
+
from django.contrib.auth import get_user_model
|
|
7
|
+
from django.db.models import Avg
|
|
6
8
|
|
|
7
9
|
hashed_storage = HashedFilenameStorage()
|
|
8
10
|
|
|
11
|
+
User = get_user_model()
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
class Tag(models.Model):
|
|
11
15
|
name = models.CharField(max_length=50, unique=True)
|
|
@@ -87,8 +91,31 @@ class Recipe(models.Model):
|
|
|
87
91
|
self.tags.set(tags)
|
|
88
92
|
return self.tags.all()
|
|
89
93
|
|
|
94
|
+
# add helper methods for ratings
|
|
95
|
+
def average_rating(self):
|
|
96
|
+
agg = self.ratings.aggregate(avg=Avg("score"))
|
|
97
|
+
return agg["avg"] or 0
|
|
98
|
+
|
|
99
|
+
def rating_count(self):
|
|
100
|
+
return self.ratings.count()
|
|
101
|
+
|
|
90
102
|
def get_absolute_url(self):
|
|
91
103
|
return reverse("recipe_detail", kwargs={"pk": self.pk, "slug": self.slug})
|
|
92
104
|
|
|
93
105
|
def __str__(self):
|
|
94
106
|
return self.title
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Rating(models.Model):
|
|
110
|
+
recipe = models.ForeignKey(Recipe, related_name="ratings", on_delete=models.CASCADE)
|
|
111
|
+
user = models.ForeignKey(User, related_name="ratings", on_delete=models.CASCADE)
|
|
112
|
+
score = models.PositiveSmallIntegerField(choices=[(i, i) for i in range(1, 6)])
|
|
113
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
114
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
115
|
+
|
|
116
|
+
class Meta:
|
|
117
|
+
unique_together = ("recipe", "user")
|
|
118
|
+
ordering = ("-updated_at",)
|
|
119
|
+
|
|
120
|
+
def __str__(self):
|
|
121
|
+
return f"{self.recipe} — {self.score} by {self.user}"
|
sandwitches/settings.py
CHANGED
|
@@ -19,11 +19,16 @@ DEBUG = bool(os.environ.get("DEBUG", default=0))
|
|
|
19
19
|
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "127.0.0.1").split(",")
|
|
20
20
|
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
|
|
21
21
|
|
|
22
|
+
# RECAPTCHA_PROXY = {'http': 'http://127.0.0.1:8000', 'https': 'https://127.0.0.1:8000'}
|
|
23
|
+
# RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY")
|
|
24
|
+
# RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY")
|
|
25
|
+
|
|
22
26
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
23
27
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}
|
|
26
30
|
|
|
31
|
+
# Application definition
|
|
27
32
|
INSTALLED_APPS = [
|
|
28
33
|
"django.contrib.admin",
|
|
29
34
|
"django.contrib.auth",
|
|
@@ -33,6 +38,7 @@ INSTALLED_APPS = [
|
|
|
33
38
|
"django.contrib.staticfiles",
|
|
34
39
|
"sandwitches",
|
|
35
40
|
"debug_toolbar",
|
|
41
|
+
# "django_recaptcha",
|
|
36
42
|
"simple_history",
|
|
37
43
|
]
|
|
38
44
|
|
sandwitches/tasks.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from django.core.mail import send_mail
|
|
3
|
+
from django.tasks import task
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@task(takes_context=True, priority=2, queue_name="emails")
|
|
10
|
+
def email_users(context, emails, subject, message):
|
|
11
|
+
logger.debug(
|
|
12
|
+
f"Attempt {context.attempt} to send user email. Task result id: {context.task_result.id}."
|
|
13
|
+
)
|
|
14
|
+
return send_mail(
|
|
15
|
+
subject=subject, message=message, from_email=None, recipient_list=emails
|
|
16
|
+
)
|
|
@@ -168,11 +168,13 @@
|
|
|
168
168
|
</li>
|
|
169
169
|
</ul>
|
|
170
170
|
<ul>
|
|
171
|
+
<li><a href="api/docs">Docs</a></li>
|
|
171
172
|
{% if user.is_authenticated %}
|
|
172
173
|
<li><a href="{% url 'admin:index' %}">Admin</a></li>
|
|
173
174
|
<li><a href="{% url 'admin:logout' %}">Logout</a></li>
|
|
174
175
|
{% else %}
|
|
175
176
|
<li><a href="{% url 'admin:login' %}">Login</a></li>
|
|
177
|
+
<li><a href="{% url 'signup' %}">Sign up</a></li>
|
|
176
178
|
{% endif %}
|
|
177
179
|
</ul>
|
|
178
180
|
</nav>
|
|
@@ -34,6 +34,66 @@
|
|
|
34
34
|
{% endfor %}
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
|
+
|
|
38
|
+
<div style="margin-top:1rem;">
|
|
39
|
+
<h4>Rating</h4>
|
|
40
|
+
{% if rating_count %}
|
|
41
|
+
<p>Average: {{ avg_rating|floatformat:1 }} ({{ rating_count }} vote{% if rating_count %}s{% endif %})</p>
|
|
42
|
+
{% else %}
|
|
43
|
+
<p>No ratings yet.</p>
|
|
44
|
+
{% endif %}
|
|
45
|
+
|
|
46
|
+
{% if user.is_authenticated %}
|
|
47
|
+
{% if user_rating %}
|
|
48
|
+
<p>Your rating: {{ user_rating.score }}</p>
|
|
49
|
+
{% else %}
|
|
50
|
+
<form method="post" action="{% url 'recipe_rate' pk=recipe.pk %}">
|
|
51
|
+
{% csrf_token %}
|
|
52
|
+
|
|
53
|
+
<fieldset>
|
|
54
|
+
<legend>What would you rate this sandwich?</legend>
|
|
55
|
+
|
|
56
|
+
{% if rating_form.score.errors %}
|
|
57
|
+
<div class="card-panel" role="alert">
|
|
58
|
+
<ul>
|
|
59
|
+
{% for err in rating_form.score.errors %}
|
|
60
|
+
<li>{{ err }}</li>
|
|
61
|
+
{% endfor %}
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
{% endif %}
|
|
65
|
+
|
|
66
|
+
<div class="row">
|
|
67
|
+
{% for i in "12345"|make_list %}
|
|
68
|
+
<div class="col-sm-2">
|
|
69
|
+
<label class="form-check">
|
|
70
|
+
<input
|
|
71
|
+
type="radio"
|
|
72
|
+
name="{{ rating_form.score.name }}"
|
|
73
|
+
id="rating-{{ i }}"
|
|
74
|
+
value="{{ i }}"
|
|
75
|
+
{% if user_rating and user_rating.score|stringformat:"s" == i %}
|
|
76
|
+
checked
|
|
77
|
+
{% elif rating_form.score.value == i %}
|
|
78
|
+
checked
|
|
79
|
+
{% endif %}
|
|
80
|
+
/>
|
|
81
|
+
<span>{{ i }}</span>
|
|
82
|
+
</label>
|
|
83
|
+
</div>
|
|
84
|
+
{% endfor %}
|
|
85
|
+
</div>
|
|
86
|
+
</fieldset>
|
|
87
|
+
<p style="margin-top:0.5rem;">
|
|
88
|
+
<button type="submit">{% if user_rating %}Update{% else %}Rate{% endif %}</button>
|
|
89
|
+
</p>
|
|
90
|
+
</form>
|
|
91
|
+
{% endif %}
|
|
92
|
+
{% else %}
|
|
93
|
+
<p><a href="{% url 'admin:login' %}">Log in</a> to rate this recipe.</p>
|
|
94
|
+
{% endif %}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
37
97
|
{% if user.is_authenticated and user.is_staff %}
|
|
38
98
|
<footer>
|
|
39
99
|
<a href="/admin/sandwitches/recipe/{{ recipe.pk }}/change/">Edit</a>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{% extends "base_pico.html" %}
|
|
2
|
+
{% load static %}
|
|
3
|
+
{% block title %}Sign Up{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="container" style="max-width:720px; margin:2rem auto;">
|
|
7
|
+
<article class="card">
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<h2>Sign up</h2>
|
|
10
|
+
|
|
11
|
+
<form method="post" novalidate>
|
|
12
|
+
{% csrf_token %}
|
|
13
|
+
{% if form.non_field_errors %}
|
|
14
|
+
<div class="card-panel" role="alert">
|
|
15
|
+
<ul>
|
|
16
|
+
{% for err in form.non_field_errors %}
|
|
17
|
+
<li>{{ err }}</li>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</ul>
|
|
20
|
+
</div>
|
|
21
|
+
{% endif %}
|
|
22
|
+
|
|
23
|
+
<label for="{{ form.username.id_for_label }}">Username</label>
|
|
24
|
+
{{ form.username }}
|
|
25
|
+
|
|
26
|
+
<label for="{{ form.email.id_for_label }}">Email (optional)</label>
|
|
27
|
+
{{ form.email }}
|
|
28
|
+
|
|
29
|
+
<label for="{{ form.first_name.id_for_label }}">First name</label>
|
|
30
|
+
{{ form.first_name }}
|
|
31
|
+
|
|
32
|
+
<label for="{{ form.last_name.id_for_label }}">Last name</label>
|
|
33
|
+
{{ form.last_name }}
|
|
34
|
+
|
|
35
|
+
<label for="{{ form.password1.id_for_label }}">Password</label>
|
|
36
|
+
{{ form.password1 }}
|
|
37
|
+
|
|
38
|
+
<label for="{{ form.password2.id_for_label }}">Confirm password</label>
|
|
39
|
+
{{ form.password2 }}
|
|
40
|
+
|
|
41
|
+
<p style="margin-top:1rem;">
|
|
42
|
+
<button type="submit">Sign Up</button>
|
|
43
|
+
<a class="contrast" href="{% url 'index' %}">Cancel</a>
|
|
44
|
+
</p>
|
|
45
|
+
</form>
|
|
46
|
+
</div>
|
|
47
|
+
</article>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{% endblock %}
|
sandwitches/urls.py
CHANGED
|
@@ -18,6 +18,8 @@ Including another URLconf
|
|
|
18
18
|
from django.contrib import admin
|
|
19
19
|
from django.urls import path
|
|
20
20
|
from . import views
|
|
21
|
+
from .api import api
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
from django.conf import settings
|
|
23
25
|
from django.conf.urls.static import static
|
|
@@ -31,6 +33,9 @@ urlpatterns = [
|
|
|
31
33
|
path("admin/", admin.site.urls),
|
|
32
34
|
path("recipes/<slug:slug>/", views.recipe_detail, name="recipe_detail"),
|
|
33
35
|
path("setup/", views.setup, name="setup"),
|
|
36
|
+
path("api/", api.urls),
|
|
37
|
+
path("signup/", views.signup, name="signup"),
|
|
38
|
+
path("recipes/<int:pk>/rate/", views.recipe_rate, name="recipe_rate"),
|
|
34
39
|
]
|
|
35
40
|
|
|
36
41
|
if "test" not in sys.argv or "PYTEST_VERSION" in os.environ:
|
sandwitches/views.py
CHANGED
|
@@ -3,9 +3,10 @@ from django.urls import reverse
|
|
|
3
3
|
from django.contrib import messages
|
|
4
4
|
from django.contrib.auth import login
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.decorators import login_required
|
|
6
7
|
|
|
7
|
-
from .models import Recipe
|
|
8
|
-
from .forms import RecipeForm, AdminSetupForm
|
|
8
|
+
from .models import Recipe, Rating
|
|
9
|
+
from .forms import RecipeForm, AdminSetupForm, UserSignupForm, RatingForm
|
|
9
10
|
|
|
10
11
|
User = get_user_model()
|
|
11
12
|
|
|
@@ -24,7 +25,50 @@ def recipe_edit(request, pk):
|
|
|
24
25
|
|
|
25
26
|
def recipe_detail(request, slug):
|
|
26
27
|
recipe = get_object_or_404(Recipe, slug=slug)
|
|
27
|
-
|
|
28
|
+
avg = recipe.average_rating()
|
|
29
|
+
count = recipe.rating_count()
|
|
30
|
+
user_rating = None
|
|
31
|
+
rating_form = None
|
|
32
|
+
if request.user.is_authenticated:
|
|
33
|
+
try:
|
|
34
|
+
user_rating = Rating.objects.get(recipe=recipe, user=request.user)
|
|
35
|
+
except Rating.DoesNotExist:
|
|
36
|
+
user_rating = None
|
|
37
|
+
# show form prefilled when possible
|
|
38
|
+
initial = {"score": str(user_rating.score)} if user_rating else None
|
|
39
|
+
rating_form = RatingForm(initial=initial)
|
|
40
|
+
return render(
|
|
41
|
+
request,
|
|
42
|
+
"detail.html",
|
|
43
|
+
{
|
|
44
|
+
"recipe": recipe,
|
|
45
|
+
"avg_rating": avg,
|
|
46
|
+
"rating_count": count,
|
|
47
|
+
"user_rating": user_rating,
|
|
48
|
+
"rating_form": rating_form,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@login_required
|
|
54
|
+
def recipe_rate(request, pk):
|
|
55
|
+
"""
|
|
56
|
+
Create or update a rating for the given recipe by the logged-in user.
|
|
57
|
+
"""
|
|
58
|
+
recipe = get_object_or_404(Recipe, pk=pk)
|
|
59
|
+
if request.method != "POST":
|
|
60
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
61
|
+
|
|
62
|
+
form = RatingForm(request.POST)
|
|
63
|
+
if form.is_valid():
|
|
64
|
+
score = int(form.cleaned_data["score"])
|
|
65
|
+
Rating.objects.update_or_create(
|
|
66
|
+
recipe=recipe, user=request.user, defaults={"score": score}
|
|
67
|
+
)
|
|
68
|
+
messages.success(request, "Your rating has been saved.")
|
|
69
|
+
else:
|
|
70
|
+
messages.error(request, "Could not save rating.")
|
|
71
|
+
return redirect("recipe_detail", slug=recipe.slug)
|
|
28
72
|
|
|
29
73
|
|
|
30
74
|
def index(request):
|
|
@@ -56,3 +100,22 @@ def setup(request):
|
|
|
56
100
|
form = AdminSetupForm()
|
|
57
101
|
|
|
58
102
|
return render(request, "setup.html", {"form": form})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def signup(request):
|
|
106
|
+
"""
|
|
107
|
+
User signup page: create new regular user accounts.
|
|
108
|
+
"""
|
|
109
|
+
if request.method == "POST":
|
|
110
|
+
form = UserSignupForm(request.POST)
|
|
111
|
+
if form.is_valid():
|
|
112
|
+
user = form.save()
|
|
113
|
+
# log in the newly created user
|
|
114
|
+
user.backend = "django.contrib.auth.backends.ModelBackend"
|
|
115
|
+
login(request, user)
|
|
116
|
+
messages.success(request, "Account created and signed in.")
|
|
117
|
+
return redirect("index")
|
|
118
|
+
else:
|
|
119
|
+
form = UserSignupForm()
|
|
120
|
+
|
|
121
|
+
return render(request, "signup.html", {"form": form})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sandwitches
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: Martyn van Dijke
|
|
6
6
|
Author-email: Martyn van Dijke <martijnvdijke600@gmail.com>
|
|
7
7
|
Requires-Dist: django-debug-toolbar>=6.1.0
|
|
8
8
|
Requires-Dist: django-filter>=25.2
|
|
9
|
+
Requires-Dist: django-ninja>=1.5.1
|
|
9
10
|
Requires-Dist: django-simple-history>=3.10.1
|
|
10
|
-
Requires-Dist: django>=
|
|
11
|
-
Requires-Dist: djangorestframework>=3.16.1
|
|
11
|
+
Requires-Dist: django>=6.0.0
|
|
12
12
|
Requires-Dist: gunicorn>=23.0.0
|
|
13
13
|
Requires-Dist: markdown>=3.10
|
|
14
14
|
Requires-Dist: pillow>=12.0.0
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
sandwitches/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sandwitches/admin.py,sha256=8QyK9xOUvGGPz30-7p2phyc-Ks9VKD_K1kCAagisPwA,120
|
|
3
|
+
sandwitches/api.py,sha256=HaTOgqGZK4sPUoGx1IFJoUQntcB1EX91pnyM7-TREjg,1565
|
|
3
4
|
sandwitches/asgi.py,sha256=cygnXdXSSVspM7ZXuj47Ef6oz7HSTw4D7BPzgE2PU5w,399
|
|
4
|
-
sandwitches/forms.py,sha256=
|
|
5
|
+
sandwitches/forms.py,sha256=NeGUi3xPzQpgw2cVWpo2ZKpYUXsCuJ3SzgGUkvxdV2A,2610
|
|
5
6
|
sandwitches/migrations/0001_initial.py,sha256=01IfkFbUyYMpTHV5GaBxJEKjzRIdUdPR5sY3AUTODww,2546
|
|
6
7
|
sandwitches/migrations/0002_historicalrecipe.py,sha256=yU2KYssfjYhPXRYN6C8IMRFr-4QUGJozz-O167XuabM,2499
|
|
8
|
+
sandwitches/migrations/0003_rating.py,sha256=iKk9M9lcBS5LwsJkMYsmYfHKqKs2QRTILgINbnilASM,1857
|
|
7
9
|
sandwitches/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
sandwitches/models.py,sha256=
|
|
9
|
-
sandwitches/settings.py,sha256=
|
|
10
|
+
sandwitches/models.py,sha256=7dJ5vHZ1bQWG2ebAJKEwaPHMukgdPth7i6quswRldmw,3974
|
|
11
|
+
sandwitches/settings.py,sha256=ApYwzWeJGX5Af4zJ6wWKq0-u8Lro8RVfO7xS7PRbpmw,3829
|
|
10
12
|
sandwitches/storage.py,sha256=XrzMw8mcUowEoV5hYjP-ZI27C3vfk010UkukA5SrGDk,917
|
|
13
|
+
sandwitches/tasks.py,sha256=rsCpfCwcIoSthpN0IZ_dt2jPsMSUe8otBmE2UbFeKg0,468
|
|
11
14
|
sandwitches/templates/base.html,sha256=gUOxxwD7G8VeBranSYp40PCWsJ-ywf8e7Qyv8VoqRP4,494
|
|
12
|
-
sandwitches/templates/base_pico.html,sha256=
|
|
13
|
-
sandwitches/templates/detail.html,sha256=
|
|
15
|
+
sandwitches/templates/base_pico.html,sha256=ntvh9wrfVqkqI58LregrPuVr9czsYB75n7HazbbkFhM,7477
|
|
16
|
+
sandwitches/templates/detail.html,sha256=1GuCwmIV-k9ROhR9bddv19rjqgC5CbLs3AzounmnAEE,3723
|
|
14
17
|
sandwitches/templates/form.html,sha256=XgrQfb_ZEJfnAou7z3gxcj7wqZ4fwFANplvYdzXg80A,373
|
|
15
18
|
sandwitches/templates/index.html,sha256=fnsU5N5dPaa3BXOoIUvgD-9a3EPz0Fs25M0p_eQnf5w,2307
|
|
16
19
|
sandwitches/templates/setup.html,sha256=Y45l96Tl6ln9Mb5gCC4JnT1bf-zuQOpYYL1JL3k-1T0,1677
|
|
20
|
+
sandwitches/templates/signup.html,sha256=UR5Doueq58hSUQgqDrhH5y5Ivdxp_8cfZ0TPaTW_IrI,1439
|
|
17
21
|
sandwitches/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
22
|
sandwitches/templatetags/markdown_extras.py,sha256=0ibmRzxE3r85x4k7kK71R-9UT0CgeegYF7MHzj3juTI,344
|
|
19
|
-
sandwitches/urls.py,sha256=
|
|
20
|
-
sandwitches/views.py,sha256=
|
|
23
|
+
sandwitches/urls.py,sha256=dBZ4o5lQqAapIVEIZ1OkNTWmU16tUoG3LygusDREaHY,1573
|
|
24
|
+
sandwitches/views.py,sha256=W8HvLFkdmS-nDj3sDY8IZWXv4V7CU5riQy75HSkbEqM,4001
|
|
21
25
|
sandwitches/wsgi.py,sha256=Eyncpnahq_4s3Lr9ruB-R3Lu9j9zBXqgPbUj7qhIbwU,399
|
|
22
|
-
sandwitches-1.0.
|
|
23
|
-
sandwitches-1.0.
|
|
24
|
-
sandwitches-1.0.
|
|
26
|
+
sandwitches-1.1.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
27
|
+
sandwitches-1.1.0.dist-info/METADATA,sha256=f4X5Ovu99_ADGnBoWqgWGP5xZKmU6Y5WWFzWQqtrRvA,613
|
|
28
|
+
sandwitches-1.1.0.dist-info/RECORD,,
|
|
File without changes
|