feat: 📘 add recipes to posts
This commit is contained in:
parent
e48e88ead2
commit
6f3ed061b4
17 changed files with 433 additions and 7 deletions
|
|
@ -21,6 +21,14 @@ from django.urls import include, path
|
||||||
|
|
||||||
from moku.views.auth import LoginView, LogoutView
|
from moku.views.auth import LoginView, LogoutView
|
||||||
from moku.views.post import FeedView
|
from moku.views.post import FeedView
|
||||||
|
from moku.views.recipe import (
|
||||||
|
DeleteRecipeView,
|
||||||
|
DeleteStepView,
|
||||||
|
EditStepView,
|
||||||
|
IndexRecipeView,
|
||||||
|
NewRecipeView,
|
||||||
|
ShowRecipeView,
|
||||||
|
)
|
||||||
from moku.views.static import ChangelogView, PrivacyView, TermsView
|
from moku.views.static import ChangelogView, PrivacyView, TermsView
|
||||||
from moku.views.user import EditProfileView, EditSettingsView, ProfileView, SignupView
|
from moku.views.user import EditProfileView, EditSettingsView, ProfileView, SignupView
|
||||||
|
|
||||||
|
|
@ -36,6 +44,12 @@ urlpatterns = [
|
||||||
path("privacy", PrivacyView.as_view(), name="privacy"),
|
path("privacy", PrivacyView.as_view(), name="privacy"),
|
||||||
path("terms", TermsView.as_view(), name="terms"),
|
path("terms", TermsView.as_view(), name="terms"),
|
||||||
path("user/<str:username>", ProfileView.as_view(), name="profile"),
|
path("user/<str:username>", ProfileView.as_view(), name="profile"),
|
||||||
|
path("recipes", IndexRecipeView.as_view(), name="recipe.index"),
|
||||||
|
path("recipes/new", NewRecipeView.as_view(), name="recipe.new"),
|
||||||
|
path("recipes/<str:uuid>", ShowRecipeView.as_view(), name="recipe.show"),
|
||||||
|
path("recipes/<str:uuid>/delete", DeleteRecipeView.as_view(), name="recipe.delete"),
|
||||||
|
path("recipes/<str:uuid>/<str:step>", EditStepView.as_view(), name="step.edit"),
|
||||||
|
path("recipes/<str:uuid>/<str:step>/delete", DeleteStepView.as_view(), name="step.delete"),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG_TOOLBAR:
|
if settings.DEBUG_TOOLBAR:
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ from moku.models import Post
|
||||||
class PostForm(ModelForm):
|
class PostForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
fields = ("emoji", "verb", "food", "image")
|
fields = ("emoji", "verb", "food", "recipe", "image")
|
||||||
labels = {
|
labels = {
|
||||||
"emoji": _("emoji"),
|
"emoji": _("emoji"),
|
||||||
"verb": _("verb"),
|
"verb": _("verb"),
|
||||||
"food": _("food"),
|
"food": _("food"),
|
||||||
|
"recipe": _("recipe"),
|
||||||
"image": _("image"),
|
"image": _("image"),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
moku/forms/recipe.py
Normal file
19
moku/forms/recipe.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from django.forms import Form, ModelForm
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from moku.models.recipe import Recipe, RecipeStep
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Recipe
|
||||||
|
fields = ("title",)
|
||||||
|
labels = {
|
||||||
|
"title": _("recipe title"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeStepForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = RecipeStep
|
||||||
|
fields = ("instructions",)
|
||||||
42
moku/migrations/0006_recipe_post_recipe_recipestep.py
Normal file
42
moku/migrations/0006_recipe_post_recipe_recipestep.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 5.0.3 on 2024-03-25 21:09
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import shortuuid.django_fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('moku', '0005_usersettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recipe',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', shortuuid.django_fields.ShortUUIDField(alphabet=None, help_text='the unique id that identifies this recipe.', length=22, max_length=22, prefix='', verbose_name='unique id')),
|
||||||
|
('title', models.CharField(help_text='give the recipe a title, just so you know the recipe is for.', max_length=64, verbose_name='recipe title')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('created_by', models.ForeignKey(db_column='created_by_user_id', on_delete=django.db.models.deletion.CASCADE, related_name='recipes', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='post',
|
||||||
|
name='recipe',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='the recipe for what you ate, if you have one.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='moku.recipe', verbose_name='recipe'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecipeStep',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', shortuuid.django_fields.ShortUUIDField(alphabet=None, help_text='the unique id that identifies this step.', length=22, max_length=22, prefix='', verbose_name='step id')),
|
||||||
|
('instructions', models.CharField(help_text='the instructions for this step of the recipe. try to keep it clear and concise!', max_length=128, verbose_name='step instructions')),
|
||||||
|
('order', models.IntegerField(db_index=True, default=0, help_text='which step in the recipe is this. this affects the order the recipe steps are shown.', verbose_name='step number')),
|
||||||
|
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='moku.recipe')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
from moku.models.post import Post
|
from moku.models.post import Post
|
||||||
from moku.models.user import User
|
from moku.models.recipe import Recipe, RecipeStep
|
||||||
|
from moku.models.user import User, UserSettings
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Post",
|
"Post",
|
||||||
|
"Recipe",
|
||||||
|
"RecipeStep",
|
||||||
"User",
|
"User",
|
||||||
|
"UserSettings",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from shortuuid.django_fields import ShortUUIDField
|
from shortuuid.django_fields import ShortUUIDField
|
||||||
|
|
||||||
from moku.constants import Verbs
|
from moku.constants import Verbs
|
||||||
|
from moku.models.recipe import RecipeStep
|
||||||
from moku.validators import validate_emoji
|
from moku.validators import validate_emoji
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,6 +12,15 @@ def post_image_filename(instance, _):
|
||||||
return f"posts/{instance.created_by.username}__{instance.uuid}.webp"
|
return f"posts/{instance.created_by.username}__{instance.uuid}.webp"
|
||||||
|
|
||||||
|
|
||||||
|
class PostManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().select_related("created_by") \
|
||||||
|
.prefetch_related(
|
||||||
|
models.Prefetch("recipe__steps", queryset=RecipeStep.objects.order_by("order"))
|
||||||
|
) \
|
||||||
|
.order_by("-created_at")
|
||||||
|
|
||||||
|
|
||||||
class Post(models.Model):
|
class Post(models.Model):
|
||||||
uuid = ShortUUIDField(
|
uuid = ShortUUIDField(
|
||||||
verbose_name=_("unique id"),
|
verbose_name=_("unique id"),
|
||||||
|
|
@ -41,6 +51,15 @@ class Post(models.Model):
|
||||||
upload_to=post_image_filename,
|
upload_to=post_image_filename,
|
||||||
help_text=_("here you can upload a picture of what you ate!"),
|
help_text=_("here you can upload a picture of what you ate!"),
|
||||||
)
|
)
|
||||||
|
recipe = models.ForeignKey(
|
||||||
|
"Recipe",
|
||||||
|
verbose_name=_("recipe"),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="+",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
help_text=_("the recipe for what you ate, if you have one."),
|
||||||
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
"User",
|
"User",
|
||||||
related_name="posts",
|
related_name="posts",
|
||||||
|
|
@ -57,6 +76,8 @@ class Post(models.Model):
|
||||||
help_text=_("when this post was last updated."),
|
help_text=_("when this post was last updated."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = PostManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.text} on {self.created_at}"
|
return f"{self.text} on {self.created_at}"
|
||||||
|
|
||||||
|
|
|
||||||
76
moku/models/recipe.py
Normal file
76
moku/models/recipe.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.urls import reverse
|
||||||
|
from shortuuid.django_fields import ShortUUIDField
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset() \
|
||||||
|
.prefetch_related(
|
||||||
|
models.Prefetch("steps", queryset=RecipeStep.objects.order_by("order"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(models.Model):
|
||||||
|
uuid = ShortUUIDField(
|
||||||
|
verbose_name=_("unique id"),
|
||||||
|
max_length=22,
|
||||||
|
length=22,
|
||||||
|
help_text=_("the unique id that identifies this recipe."),
|
||||||
|
)
|
||||||
|
title = models.CharField(
|
||||||
|
verbose_name=_("recipe title"),
|
||||||
|
max_length=64,
|
||||||
|
help_text=_("give the recipe a title, just so you know the recipe is for."),
|
||||||
|
)
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
"User",
|
||||||
|
related_name="recipes",
|
||||||
|
db_index=True,
|
||||||
|
db_column="created_by_user_id",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RecipeManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title} by @{self.created_by.username}"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("recipe.show", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeStep(models.Model):
|
||||||
|
uuid = ShortUUIDField(
|
||||||
|
verbose_name=_("step id"),
|
||||||
|
max_length=22,
|
||||||
|
length=22,
|
||||||
|
help_text=_("the unique id that identifies this step."),
|
||||||
|
)
|
||||||
|
instructions = models.CharField(
|
||||||
|
verbose_name=_("step instructions"),
|
||||||
|
max_length=128,
|
||||||
|
help_text=_("the instructions for this step of the recipe. try to keep it clear and concise!"),
|
||||||
|
)
|
||||||
|
order = models.IntegerField(
|
||||||
|
verbose_name=_("step number"),
|
||||||
|
default=0,
|
||||||
|
db_index=True,
|
||||||
|
help_text=_("which step in the recipe is this. this affects the order the recipe steps are shown."),
|
||||||
|
)
|
||||||
|
recipe = models.ForeignKey(
|
||||||
|
"Recipe",
|
||||||
|
related_name="steps",
|
||||||
|
db_index=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"step #{self.order + 1} of {self.recipe}"
|
||||||
|
|
@ -39,6 +39,14 @@ body {
|
||||||
margin-block-end: 1.6rem;
|
margin-block-end: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle {
|
||||||
|
color: var(--charcoal);
|
||||||
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
margin-block-end: 1.6rem;
|
margin-block-end: 1.6rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -150,6 +158,12 @@ form .field {
|
||||||
row-gap: .4rem;
|
row-gap: .4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form .field .field-button {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
column-gap: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
form .checkbox {
|
form .checkbox {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
|
|
@ -341,13 +355,21 @@ header nav ul {
|
||||||
margin-block-start: 1.2rem;
|
margin-block-start: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block ul {
|
.block ul,
|
||||||
|
.block ol {
|
||||||
padding-inline-start: 3.2rem;
|
padding-inline-start: 3.2rem;
|
||||||
list-style: disc;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
row-gap: .4rem;
|
row-gap: .4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block ul {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
.block strong {
|
.block strong {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li><a href="{{ url('feed') }}">{% trans %}feed{% endtrans %}</a></li>
|
<li><a href="{{ url('feed') }}">{% trans %}feed{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('profile', username=request.user.username) }}">{% trans %}my profile{% endtrans %}</a></li>
|
<li><a href="{{ url('recipe.index') }}">{% trans %}recipes{% endtrans %}</a></li>
|
||||||
|
<li><a href="{{ url('profile', username=request.user.username) }}">{% trans %}profile{% endtrans %}</a></li>
|
||||||
<li><a href="{{ url('settings') }}">{% trans %}settings{% endtrans %}</a></li>
|
<li><a href="{{ url('settings') }}">{% trans %}settings{% endtrans %}</a></li>
|
||||||
<li>
|
<li>
|
||||||
<form action="{{ url('logout') }}" method="POST" class="logout">
|
<form action="{{ url('logout') }}" method="POST" class="logout">
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@
|
||||||
</select>
|
</select>
|
||||||
<span class="help" id="help_verb">{{ form.verb.help_text }}</span>
|
<span class="help" id="help_verb">{{ form.verb.help_text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_recipe">{{ form.recipe.label }}</label>
|
||||||
|
{{ form.recipe }}
|
||||||
|
<span class="help">{{ form.recipe.help_text }}</span>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="id_image">{{ form.image.label }}</label>
|
<label for="id_image">{{ form.image.label }}</label>
|
||||||
{{ form.image }}
|
{{ form.image }}
|
||||||
|
|
|
||||||
16
moku/templates/moku/recipe/edit_step.jinja
Normal file
16
moku/templates/moku/recipe/edit_step.jinja
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content">
|
||||||
|
<form action="" method="POST" class="auth">
|
||||||
|
{% include "moku/snippets/form_errors.jinja" %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_instructions">{% trans %}instructions{% endtrans %}</label>
|
||||||
|
{{ form.instructions }}
|
||||||
|
<span class="help">{{ form.instructions.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="submit">{% trans %}update!{% endtrans %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
16
moku/templates/moku/recipe/form.jinja
Normal file
16
moku/templates/moku/recipe/form.jinja
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content">
|
||||||
|
<form action="" method="POST" class="auth">
|
||||||
|
{% include "moku/snippets/form_errors.jinja" %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_title">{{ form.title.label }}</label>
|
||||||
|
{{ form.title }}
|
||||||
|
<span class="help">{{ form.title.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="submit">{% trans %}create!{% endtrans %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
16
moku/templates/moku/recipe/index.jinja
Normal file
16
moku/templates/moku/recipe/index.jinja
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content block">
|
||||||
|
<p class="mb">👏 <a href="{{ url('recipe.new') }}">new recipe</a></p>
|
||||||
|
<h2>your recipes</h2>
|
||||||
|
<ul>
|
||||||
|
{% if not recipes %}
|
||||||
|
<li>{% trans %}no recipes yet... 😴{% endtrans %}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for recipe in recipes %}
|
||||||
|
<li><a href="{{ recipe.get_absolute_url() }}">{{ recipe.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
43
moku/templates/moku/recipe/show.jinja
Normal file
43
moku/templates/moku/recipe/show.jinja
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content block">
|
||||||
|
{% if request.user.id == recipe.created_by.id %}
|
||||||
|
<p class="mb">🗑️ <a href="{{ url('recipe.delete', uuid=recipe.uuid) }}">{% trans %}delete recipe{% endtrans %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
<h2>{{ recipe.title }}</h2>
|
||||||
|
<ol>
|
||||||
|
{% set count = recipe.steps.count() %}
|
||||||
|
{% for step in recipe.steps.all() %}
|
||||||
|
<li>
|
||||||
|
{{ step.instructions }}
|
||||||
|
{% if request.user.is_authenticated and request.user.id == recipe.created_by.id %}
|
||||||
|
<span class="small subtle">
|
||||||
|
[<a href="{{ url('step.edit', uuid=recipe.uuid, step=step.uuid) }}">{% trans %}edit{% endtrans %}</a>]
|
||||||
|
{% if loop.index0 == (count-1) %}
|
||||||
|
[<a href="{{ url('step.delete', uuid=recipe.uuid, step=step.uuid) }}">{% trans %}delete{% endtrans %}</a>]
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if request.user.is_authenticated and request.user.id == recipe.created_by.id %}
|
||||||
|
{% if count < 16 %}
|
||||||
|
<li>
|
||||||
|
<form action="" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_instructions">👉 <strong>{% trans %}add a step{% endtrans %}</strong></label>
|
||||||
|
<div class="field-button">
|
||||||
|
{{ form.instructions }}
|
||||||
|
<button type="submit">{% trans %}add!{% endtrans %}</button>
|
||||||
|
</div>
|
||||||
|
<span class="help">{{ form.instructions.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -10,6 +10,15 @@
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p class="food">{{ post.text|safe }}</p>
|
<p class="food">{{ post.text|safe }}</p>
|
||||||
<p class="metadata">{{ post.created_at|naturaltime }}</p>
|
<p class="metadata">{{ post.created_at|naturaltime }}</p>
|
||||||
<!-- <details class="recipe"><summary>recipe</summary><ol>...</ol></details> -->
|
{% if post.recipe %}
|
||||||
|
<details class="recipe">
|
||||||
|
<summary>recipe</summary>
|
||||||
|
<ol>
|
||||||
|
{% for step in post.recipe.steps.all() %}
|
||||||
|
<li>{{ step.instructions }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -27,7 +27,7 @@ class FeedView(FormView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = {
|
context = {
|
||||||
**super().get_context_data(**kwargs),
|
**super().get_context_data(**kwargs),
|
||||||
"posts": Post.objects.order_by("-created_at").all()[:128]
|
"posts": Post.objects.all()[:128]
|
||||||
}
|
}
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
return self.get_authenticated_context_data(context)
|
return self.get_authenticated_context_data(context)
|
||||||
|
|
|
||||||
121
moku/views/recipe.py
Normal file
121
moku/views/recipe.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.views.generic import FormView, TemplateView
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from moku.forms.recipe import RecipeForm, RecipeStepForm
|
||||||
|
from moku.models.recipe import Recipe, RecipeStep
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRecipeView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.recipe.delete()
|
||||||
|
messages.success(self.request, _("recipe deleted successfully!"))
|
||||||
|
return redirect("recipe.index")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def recipe(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
Recipe,
|
||||||
|
uuid=self.kwargs.get("uuid"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.id == self.recipe.created_by.id
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteStepView(LoginRequiredMixin, UserPassesTestMixin, TemplateView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if self.step.order != (self.step.recipe.steps.count() - 1):
|
||||||
|
messages.error(self.request, _("sorry! you can only delete the last step."))
|
||||||
|
return redirect(self.step.recipe.get_absolute_url())
|
||||||
|
self.step.delete()
|
||||||
|
messages.success(self.request, _("step deleted!"))
|
||||||
|
return redirect(self.step.recipe.get_absolute_url())
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def step(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
RecipeStep,
|
||||||
|
recipe__uuid=self.kwargs.get("uuid"),
|
||||||
|
uuid=self.kwargs.get("step"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.id == self.step.recipe.created_by.id
|
||||||
|
|
||||||
|
|
||||||
|
class EditStepView(LoginRequiredMixin, UserPassesTestMixin, FormView):
|
||||||
|
template_name = "moku/recipe/edit_step.jinja"
|
||||||
|
form_class = RecipeStepForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
messages.success(self.request, _("step updated!"))
|
||||||
|
return redirect(self.step.recipe.get_absolute_url())
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
return self.form_class(instance=self.step, **self.get_form_kwargs())
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def step(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
RecipeStep,
|
||||||
|
recipe__uuid=self.kwargs.get("uuid"),
|
||||||
|
uuid=self.kwargs.get("step"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.id == self.step.recipe.created_by.id
|
||||||
|
|
||||||
|
|
||||||
|
class IndexRecipeView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "moku/recipe/index.jinja"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
**super().get_context_data(**kwargs),
|
||||||
|
"recipes": Recipe.objects.filter(created_by=self.request.user).order_by("-created_by"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NewRecipeView(LoginRequiredMixin, FormView):
|
||||||
|
template_name = "moku/recipe/form.jinja"
|
||||||
|
form_class = RecipeForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.created_by = self.request.user
|
||||||
|
form.save()
|
||||||
|
messages.success(self.request, _("recipe made successfully! now you can start adding the steps."))
|
||||||
|
return redirect(form.instance.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
class ShowRecipeView(FormView):
|
||||||
|
template_name = "moku/recipe/show.jinja"
|
||||||
|
form_class = RecipeStepForm
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def recipe(self):
|
||||||
|
return Recipe.objects.get(uuid=self.kwargs.get("uuid"))
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if not self.request.user.is_authenticated or self.request.user.id != self.recipe.created_by.id:
|
||||||
|
messages.error(self.request, _("that's not yours!"))
|
||||||
|
return redirect(form.instance.get_absolute_url())
|
||||||
|
order = self.recipe.steps.count()
|
||||||
|
if order >= 16:
|
||||||
|
messages.error(self.request, _("sorry! you can't add any more steps to this recipe."))
|
||||||
|
return redirect(self.recipe.get_absolute_url())
|
||||||
|
form.instance.recipe = self.recipe
|
||||||
|
form.instance.order = order
|
||||||
|
form.save()
|
||||||
|
messages.success(self.request, _("step added successfully!"))
|
||||||
|
return redirect(self.recipe.get_absolute_url())
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
**super().get_context_data(**kwargs),
|
||||||
|
"recipe": self.recipe,
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue