feat: 📝 allow editing of posts

This commit is contained in:
m5ka 2024-03-27 15:43:24 +00:00
parent 98a9809cda
commit 2fed369027
9 changed files with 159 additions and 88 deletions

View file

@ -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"),

View file

@ -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;
} }

View file

@ -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 %}

View 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 %}

View 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 %}

View file

@ -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>

View 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>

View file

@ -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):

View file

@ -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!"))