feat: 📝 allow editing of posts
This commit is contained in:
parent
98a9809cda
commit
2fed369027
9 changed files with 159 additions and 88 deletions
|
|
@ -5,7 +5,7 @@ from django.urls import include, path
|
||||||
|
|
||||||
from moku.views.auth import LoginView, LogoutView
|
from moku.views.auth import LoginView, LogoutView
|
||||||
from moku.views.blog import IndexBlogView
|
from moku.views.blog import IndexBlogView
|
||||||
from moku.views.post import FeedView
|
from moku.views.post import EditPostview, FeedView
|
||||||
from moku.views.recipe import (
|
from moku.views.recipe import (
|
||||||
DeleteRecipeView,
|
DeleteRecipeView,
|
||||||
DeleteStepView,
|
DeleteStepView,
|
||||||
|
|
@ -34,6 +34,7 @@ urlpatterns = [
|
||||||
path("blog", IndexBlogView.as_view(), name="blog.index"),
|
path("blog", IndexBlogView.as_view(), name="blog.index"),
|
||||||
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("edit/<str:uuid>", EditPostview.as_view(), name="post.edit"),
|
||||||
path("user/<str:username>", ProfileView.as_view(), name="profile"),
|
path("user/<str:username>", ProfileView.as_view(), name="profile"),
|
||||||
path("user/<str:username>/json", UserJSONView.as_view(), name="json"),
|
path("user/<str:username>/json", UserJSONView.as_view(), name="json"),
|
||||||
path("recipes", IndexRecipeView.as_view(), name="recipe.index"),
|
path("recipes", IndexRecipeView.as_view(), name="recipe.index"),
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ body {
|
||||||
color: var(--charcoal);
|
color: var(--charcoal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.subtle {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
a.subtle:hover {
|
a.subtle:hover {
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +299,7 @@ form .emoji-picker ul li input[type=radio]:checked + label {
|
||||||
form button[type=submit]:not(.logout) {
|
form button[type=submit]:not(.logout) {
|
||||||
padding: .4rem .6rem;
|
padding: .4rem .6rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
font-family: var(--font-family);
|
||||||
background: var(--tangerine);
|
background: var(--tangerine);
|
||||||
border: 1px solid var(--orange);
|
border: 1px solid var(--orange);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -430,14 +435,14 @@ header nav ul {
|
||||||
margin-block-start: 1.2rem;
|
margin-block-start: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block ul,
|
.block ul:not(.errors):not(.emoji-picker-group),
|
||||||
.block ol {
|
.block ol {
|
||||||
padding-inline-start: 3.2rem;
|
padding-inline-start: 3.2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
row-gap: .4rem;
|
row-gap: .4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block ul {
|
.block ul:not(.errors):not(.emoji-picker-group) {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
{% extends "moku/base.jinja" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="grid-content">
|
|
||||||
<aside>
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<form action="" method="POST" enctype="multipart/form-data">
|
|
||||||
{% include "moku/snippets/form_errors.jinja" %}
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="emoji-picker">
|
|
||||||
{% for emoji_category in emoji %}
|
|
||||||
{% set outer_loop = loop %}
|
|
||||||
<details{% if loop.index0 == 0 %} open{% endif %}>
|
|
||||||
<summary>{{ emoji_category[0] }}</summary>
|
|
||||||
<ul>
|
|
||||||
{% for emoji_choice in emoji_category[1] %}
|
|
||||||
{% set emoji_label = emoji_choice|unemoji %}
|
|
||||||
<li>
|
|
||||||
<input type="radio" value="{{ emoji_choice }}" name="emoji" id="id_emoji_{{ emoji_label }}" required{% if loop.index0 == 0 and outer_loop.index0 == 0 %} checked{% endif %}>
|
|
||||||
<label for="id_emoji_{{ emoji_label }}">{{ emoji_choice }}</label>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="id_food">{{ form.food.label }}</label>
|
|
||||||
<input type="text" name="food" id="id_food" required aria-describedby="help_food">
|
|
||||||
<span class="help" id="help_food">{{ form.food.help_text }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="id_verb">{{ form.verb.label }}</label>
|
|
||||||
<select name="verb" id="id_verb">
|
|
||||||
{% for verb, verb_label in verbs %}
|
|
||||||
<option value="{{ verb }}">{{ verb_label }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<span class="help" id="help_verb">{{ form.verb.help_text }}</span>
|
|
||||||
</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">
|
|
||||||
<label for="id_image">{{ form.image.label }}</label>
|
|
||||||
{{ form.image }}
|
|
||||||
<span class="help" id="help_image">{{ form.image.help_text }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<button type="submit">{% trans %}post!{% endtrans %}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<p>{% trans %}want to post?{% endtrans %}</p>
|
|
||||||
<p>
|
|
||||||
{% with login_url=url('login'), signup_url=url('signup') %}
|
|
||||||
{% trans %}<a href="{{ login_url }}">log in</a> or <a href="{{ signup_url }}">make an account</a>!{% endtrans %}
|
|
||||||
{% endwith %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</aside>
|
|
||||||
<main>
|
|
||||||
{% if not posts %}
|
|
||||||
<p>{% trans %}no posts yet...{% endtrans %} 🥱</p>
|
|
||||||
{% endif %}
|
|
||||||
{% for post in posts %}
|
|
||||||
{% include "moku/snippets/post.jinja" %}
|
|
||||||
{% endfor %}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
8
moku/templates/moku/post/edit.jinja
Normal file
8
moku/templates/moku/post/edit.jinja
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content block">
|
||||||
|
<h2>{% trans time_ago=post.created_at|naturaltime %}editing post from {{ time_ago }}{% endtrans %}</h2>
|
||||||
|
{% include "moku/snippets/post_form.jinja" %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
26
moku/templates/moku/post/index.jinja
Normal file
26
moku/templates/moku/post/index.jinja
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "moku/base.jinja" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-content">
|
||||||
|
<aside>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
{% include "moku/snippets/post_form.jinja" %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans %}want to post?{% endtrans %}</p>
|
||||||
|
<p>
|
||||||
|
{% with login_url=url('login'), signup_url=url('signup') %}
|
||||||
|
{% trans %}<a href="{{ login_url }}">log in</a> or <a href="{{ signup_url }}">make an account</a>!{% endtrans %}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
{% if not posts %}
|
||||||
|
<p>{% trans %}no posts yet...{% endtrans %} 🥱</p>
|
||||||
|
{% endif %}
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include "moku/snippets/post.jinja" %}
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -9,7 +9,13 @@
|
||||||
<div class="emoji">{{ post.emoji }}</div>
|
<div class="emoji">{{ post.emoji }}</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<p class="food user-content">{{ post.text|safe }}</p>
|
<p class="food user-content">{{ post.text|safe }}</p>
|
||||||
<p class="metadata">{{ post.created_at|naturaltime }}</p>
|
<p class="metadata">
|
||||||
|
{{ post.created_at|naturaltime }}
|
||||||
|
{% if request.user.is_authenticated and request.user.id == post.created_by.id %}
|
||||||
|
·
|
||||||
|
<a href="{{ url('post.edit', uuid=post.uuid) }}" class="subtle">edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
{% if post.recipe %}
|
{% if post.recipe %}
|
||||||
<details class="recipe">
|
<details class="recipe">
|
||||||
<summary>{% trans %}recipe{% endtrans %}</summary>
|
<summary>{% trans %}recipe{% endtrans %}</summary>
|
||||||
|
|
|
||||||
55
moku/templates/moku/snippets/post_form.jinja
Normal file
55
moku/templates/moku/snippets/post_form.jinja
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<form action="" method="POST" enctype="multipart/form-data">
|
||||||
|
{% include "moku/snippets/form_errors.jinja" %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="emoji-picker">
|
||||||
|
{% set current_emoji = form.emoji.value() or emoji[0][1][0] %}
|
||||||
|
{% for emoji_category in emoji %}
|
||||||
|
<details{% if current_emoji in emoji_category[1] %} open{% endif %}>
|
||||||
|
<summary>{{ emoji_category[0] }}</summary>
|
||||||
|
<ul class="emoji-picker-group">
|
||||||
|
{% for emoji_choice in emoji_category[1] %}
|
||||||
|
{% set emoji_label = emoji_choice|unemoji %}
|
||||||
|
<li>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="{{ emoji_choice }}"
|
||||||
|
name="emoji"
|
||||||
|
id="id_emoji_{{ emoji_label }}"
|
||||||
|
required
|
||||||
|
{% if emoji_choice == current_emoji %} checked{% endif %}
|
||||||
|
>
|
||||||
|
<label for="id_emoji_{{ emoji_label }}">{{ emoji_choice }}</label>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_food">{{ form.food.label }}</label>
|
||||||
|
<input type="text" name="food" id="id_food" value="{{ form.food.value() or "" }}" required aria-describedby="help_food">
|
||||||
|
<span class="help" id="help_food">{{ form.food.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="id_verb">{{ form.verb.label }}</label>
|
||||||
|
<select name="verb" id="id_verb">
|
||||||
|
{% for verb, verb_label in verbs %}
|
||||||
|
<option value="{{ verb }}"{% if form.verb.value() == verb %} selected{% endif %}>{{ verb_label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span class="help" id="help_verb">{{ form.verb.help_text }}</span>
|
||||||
|
</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">
|
||||||
|
<label for="id_image">{{ form.image.label }}</label>
|
||||||
|
<div>{{ form.image }}</div>
|
||||||
|
<span class="help" id="help_image">{{ form.image.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button type="submit">{% trans %}post!{% endtrans %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from moku.constants import EMOJI_CATEGORIES, Verbs
|
from moku.constants import EMOJI_CATEGORIES, Verbs
|
||||||
|
|
@ -11,10 +13,57 @@ from moku.models.recipe import Recipe
|
||||||
from moku.views.base import FormView
|
from moku.views.base import FormView
|
||||||
|
|
||||||
|
|
||||||
|
def _get_verbs(username):
|
||||||
|
return (
|
||||||
|
(verb[0], verb[1] % {"user": f"@{username}", "food": "..."})
|
||||||
|
for verb in Verbs.CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditPostview(LoginRequiredMixin, UserPassesTestMixin, FormView):
|
||||||
|
"""Allows users to edit their previous posts."""
|
||||||
|
|
||||||
|
template_name = "moku/post/edit.jinja"
|
||||||
|
form_class = PostForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if (
|
||||||
|
form.instance.recipe
|
||||||
|
and form.instance.recipe.created_by.id != self.request.user.id
|
||||||
|
):
|
||||||
|
messages.error(
|
||||||
|
self.request, _("you can't add someone else's recipe to your post!")
|
||||||
|
)
|
||||||
|
return self.form_invalid(form)
|
||||||
|
if "image" in form.changed_data and form.instance.image.name:
|
||||||
|
form.instance.image = process_post_image(form.instance.image)
|
||||||
|
form.save()
|
||||||
|
messages.success(self.request, _("your post was updated!"))
|
||||||
|
return redirect("feed")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
return {
|
||||||
|
**super().get_context_data(**kwargs),
|
||||||
|
"post": self.post_object,
|
||||||
|
"verbs": _get_verbs(self.request.user.username),
|
||||||
|
"emoji": EMOJI_CATEGORIES,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
return self.form_class(instance=self.post_object, **self.get_form_kwargs())
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def post_object(self):
|
||||||
|
return get_object_or_404(Post, uuid=self.kwargs.get("uuid"))
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.post_object.created_by.id == self.request.user.id
|
||||||
|
|
||||||
|
|
||||||
class FeedView(FormView):
|
class FeedView(FormView):
|
||||||
"""Allows users to see recent posts and create a new post."""
|
"""Allows users to see recent posts and create a new post."""
|
||||||
|
|
||||||
template_name = "moku/feed.jinja"
|
template_name = "moku/post/index.jinja"
|
||||||
form_class = PostForm
|
form_class = PostForm
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|
@ -29,7 +78,7 @@ class FeedView(FormView):
|
||||||
)
|
)
|
||||||
return redirect("feed")
|
return redirect("feed")
|
||||||
form.instance.created_by = self.request.user
|
form.instance.created_by = self.request.user
|
||||||
if "image" in form.changed_data and form.instance.image is not None:
|
if "image" in form.changed_data and form.instance.image.name:
|
||||||
form.instance.image = process_post_image(form.instance.image)
|
form.instance.image = process_post_image(form.instance.image)
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(self.request, _("your post was made!"))
|
messages.success(self.request, _("your post was made!"))
|
||||||
|
|
@ -48,13 +97,7 @@ class FeedView(FormView):
|
||||||
return {
|
return {
|
||||||
**context,
|
**context,
|
||||||
"emoji": EMOJI_CATEGORIES,
|
"emoji": EMOJI_CATEGORIES,
|
||||||
"verbs": (
|
"verbs": _get_verbs(self.request.user.username),
|
||||||
(
|
|
||||||
verb[0],
|
|
||||||
verb[1] % {"user": f"@{self.request.user.username}", "food": "..."},
|
|
||||||
)
|
|
||||||
for verb in Verbs.CHOICES
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class EditProfileView(LoginRequiredMixin, FormView):
|
||||||
page_title = gettext_lazy("edit profile")
|
page_title = gettext_lazy("edit profile")
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
if "avatar" in form.changed_data and form.instance.avatar is not None:
|
if "avatar" in form.changed_data and form.instance.avatar.name:
|
||||||
form.instance.avatar = process_avatar_image(form.instance.avatar)
|
form.instance.avatar = process_avatar_image(form.instance.avatar)
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(self.request, _("profile updated successfully!"))
|
messages.success(self.request, _("profile updated successfully!"))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue