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.blog import IndexBlogView
|
||||
from moku.views.post import FeedView
|
||||
from moku.views.post import EditPostview, FeedView
|
||||
from moku.views.recipe import (
|
||||
DeleteRecipeView,
|
||||
DeleteStepView,
|
||||
|
|
@ -34,6 +34,7 @@ urlpatterns = [
|
|||
path("blog", IndexBlogView.as_view(), name="blog.index"),
|
||||
path("privacy", PrivacyView.as_view(), name="privacy"),
|
||||
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>/json", UserJSONView.as_view(), name="json"),
|
||||
path("recipes", IndexRecipeView.as_view(), name="recipe.index"),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ body {
|
|||
color: var(--charcoal);
|
||||
}
|
||||
|
||||
a.subtle {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a.subtle:hover {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
|
@ -295,6 +299,7 @@ form .emoji-picker ul li input[type=radio]:checked + label {
|
|||
form button[type=submit]:not(.logout) {
|
||||
padding: .4rem .6rem;
|
||||
font-size: 1.5rem;
|
||||
font-family: var(--font-family);
|
||||
background: var(--tangerine);
|
||||
border: 1px solid var(--orange);
|
||||
outline: none;
|
||||
|
|
@ -430,14 +435,14 @@ header nav ul {
|
|||
margin-block-start: 1.2rem;
|
||||
}
|
||||
|
||||
.block ul,
|
||||
.block ul:not(.errors):not(.emoji-picker-group),
|
||||
.block ol {
|
||||
padding-inline-start: 3.2rem;
|
||||
display: grid;
|
||||
row-gap: .4rem;
|
||||
}
|
||||
|
||||
.block ul {
|
||||
.block ul:not(.errors):not(.emoji-picker-group) {
|
||||
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="body">
|
||||
<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 %}
|
||||
<details class="recipe">
|
||||
<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.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
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 moku.constants import EMOJI_CATEGORIES, Verbs
|
||||
|
|
@ -11,10 +13,57 @@ from moku.models.recipe import Recipe
|
|||
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):
|
||||
"""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
|
||||
|
||||
def form_valid(self, form):
|
||||
|
|
@ -29,7 +78,7 @@ class FeedView(FormView):
|
|||
)
|
||||
return redirect("feed")
|
||||
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.save()
|
||||
messages.success(self.request, _("your post was made!"))
|
||||
|
|
@ -48,13 +97,7 @@ class FeedView(FormView):
|
|||
return {
|
||||
**context,
|
||||
"emoji": EMOJI_CATEGORIES,
|
||||
"verbs": (
|
||||
(
|
||||
verb[0],
|
||||
verb[1] % {"user": f"@{self.request.user.username}", "food": "..."},
|
||||
)
|
||||
for verb in Verbs.CHOICES
|
||||
),
|
||||
"verbs": _get_verbs(self.request.user.username),
|
||||
}
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class EditProfileView(LoginRequiredMixin, FormView):
|
|||
page_title = gettext_lazy("edit profile")
|
||||
|
||||
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.save()
|
||||
messages.success(self.request, _("profile updated successfully!"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue