feature: 🖼️ add user avatars

This commit is contained in:
m5ka 2024-03-25 14:11:21 +00:00
parent 9abf17db4c
commit bf3775d401
15 changed files with 92 additions and 35 deletions

1
.gitignore vendored
View file

@ -76,3 +76,4 @@ dmypy.json
.pytype/
cython_debug/
.idea/
media/

View file

@ -71,7 +71,7 @@ TEMPLATES = [
"django.contrib.messages.context_processors.messages",
],
"filters": {
"unemoji": "moku.utils.unemoji",
"unemoji": "moku.filters.unemoji",
},
"policies": {"ext.i18n.trimmed": True},
},

5
moku/filters.py Normal file
View file

@ -0,0 +1,5 @@
from emoji import demojize
def unemoji(txt: str):
return demojize(txt, delimiters=("", ""))

View file

@ -1,5 +1,4 @@
from django import forms
from django.contrib.auth import password_validation
from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
@ -35,8 +34,9 @@ class UserForm(UserCreationForm):
class ProfileForm(forms.ModelForm):
class Meta:
model = User
fields = ("pronouns", "location", "bio")
fields = ("avatar", "pronouns", "location", "bio")
labels = {
"avatar": _("avatar"),
"pronouns": _("pronouns"),
"location": _("location"),
"bio": _("about me"),

21
moku/images.py Normal file
View file

@ -0,0 +1,21 @@
from io import BytesIO
from django.core.files import File
from PIL import Image
def _convert_image_to_webp(image_file):
image = Image.open(image_file)
image.convert("RGB")
image.thumbnail((486, 486))
thumb_io = BytesIO()
image.save(thumb_io, "WEBP")
return File(thumb_io, name=image_file.name)
def process_avatar_image(image_file):
return _convert_image_to_webp(image_file)
def process_post_image(image_file):
return _convert_image_to_webp(image_file)

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.3 on 2024-03-25 13:49
import moku.models.user
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('moku', '0002_post'),
]
operations = [
migrations.AddField(
model_name='user',
name='avatar',
field=models.ImageField(blank=True, help_text='a little picture to show up on your profile.', upload_to=moku.models.user.user_avatar_filename, verbose_name='avatar'),
),
]

View file

@ -4,14 +4,11 @@ from django.utils.translation import gettext_lazy as _
from shortuuid.django_fields import ShortUUIDField
from moku.constants import Verbs
from moku.utils import process_image
from moku.validators import validate_emoji
def post_image_filename(instance, filename):
fn = filename.split(".")
ext = "png" if len(fn) < 2 else fn[-1]
return f"posts/{instance.created_by.username}__{instance.uuid}.{ext}"
def post_image_filename(instance, _):
return f"posts/{instance.created_by.username}__{instance.uuid}.webp"
class Post(models.Model):
@ -63,11 +60,6 @@ class Post(models.Model):
def __str__(self):
return self.uuid
def save(self, *args, **kwargs):
if not self.id and self.image:
self.image = process_image(self.image)
super().save(*args, **kwargs)
@property
def text(self):
return self.get_verb_display() % {

View file

@ -6,6 +6,10 @@ from django.urls import reverse
from moku.validators import validate_username_regex, validate_username_length
def user_avatar_filename(instance, _):
return f"avatars/{instance.username}.webp"
class User(AbstractUser):
username = models.CharField(
verbose_name=_("username"),
@ -48,6 +52,12 @@ class User(AbstractUser):
blank=True,
help_text=_("write something about yourself!"),
)
avatar = models.ImageField(
verbose_name=_("avatar"),
blank=True,
upload_to=user_avatar_filename,
help_text=_("a little picture to show up on your profile."),
)
last_seen_at = models.DateTimeField(
verbose_name=_("last seen at"),
blank=True,

View file

@ -87,6 +87,14 @@ a:hover, button.logout:hover {
color: var(--orange);
}
.avatar {
border-radius: 8px;
}
aside .avatar {
max-width: 100%;
}
dl {
display: grid;
grid-template-columns: 1fr 1fr;
@ -148,7 +156,7 @@ form .field .help {
font-size: 1.4rem;
}
form input,
form input:not([name$="-clear"]),
form select {
width: 100%;
}

View file

@ -4,7 +4,7 @@
<div class="grid-content">
<aside>
{% if request.user.is_authenticated %}
<form action="" method="POST" enctype="{% if form.is_multipart %}multipart/form-data{% else %}application/x-www-form-urlencoded{% endif %}">
<form action="" method="POST" enctype="multipart/form-data">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="emoji-picker">

View file

@ -3,20 +3,28 @@
{% block content %}
<div class="content">
<p class="mb"><a href="{{ url('profile', username=request.user.username) }}">👈 {% trans %}back to my profile{% endtrans %}</a></p>
<form action="" method="POST" class="auth">
<form action="" method="POST" enctype="multipart/form-data" class="auth">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="field">
<label for="id_avatar">{{ form.avatar.label }}</label>
<div>{{ form.avatar }}</div>
<span class="help">{{ form.avatar.help_text }}</span>
</div>
<div class="field">
<label for="id_pronouns">{{ form.pronouns.label }}</label>
<input type="text" name="pronouns" id="id_pronouns" value="{{ form.pronouns.value() or "" }}">
<span class="help">{{ form.pronouns.help_text }}</span>
</div>
<div class="field">
<label for="id_location">{{ form.location.label }}</label>
<input type="text" name="location" id="id_location" value="{{ form.location.value() or "" }}">
<span class="help">{{ form.location.help_text }}</span>
</div>
<div class="field">
<label for="id_bio">{{ form.bio.label }}</label>
<textarea name="bio" id="id_bio">{{ form.bio.value() or "" }}</textarea>
<span class="help">{{ form.bio.help_text }}</span>
</div>
<button type="submit">update!</button>
</form>

View file

@ -5,6 +5,11 @@
<aside class="profile">
<h2>@{{ profile.username }}</h2>
<dl>
{% if profile.avatar %}
<div class="double">
<img src="{{ profile.avatar.url }}" alt="@{{ profile.username }}'s avatar" class="avatar">
</div>
{% endif %}
<div>
<dt>{% trans %}pronouns{% endtrans %}</dt>
<dd>{{ profile.pronouns or "not set" }}</dd>

View file

@ -1,18 +0,0 @@
from io import BytesIO
from django.core.files import File
from emoji import demojize
from PIL import Image
def process_image(image_file):
image = Image.open(image_file)
image.convert("RGB")
image.thumbnail((486, 486))
thumb_io = BytesIO()
image.save(thumb_io, "WEBP")
return File(thumb_io, name=".".join(image_file.name.split(".")[:-1] + ["webp"]))
def unemoji(txt: str):
return demojize(txt, delimiters=("", ""))

View file

@ -5,6 +5,7 @@ from django.utils.translation import gettext as _
from django.views.generic import FormView
from moku.constants import EMOJI_CATEGORIES, Verbs
from moku.images import process_post_image
from moku.models.post import Post
from moku.forms.post import PostForm
@ -17,6 +18,8 @@ class FeedView(FormView):
if not self.request.user.is_authenticated:
raise PermissionDenied
form.instance.created_by = self.request.user
if "image" in form.changed_data and form.instance.image is not None:
form.instance.image = process_post_image(form.instance.image)
form.save()
messages.success(self.request, _("your post was made!"))
return redirect("feed")

View file

@ -4,6 +4,7 @@ from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.generic import FormView, TemplateView
from moku.images import process_avatar_image
from moku.forms.user import ProfileForm, UserForm
from moku.models.user import User
@ -13,6 +14,8 @@ class EditProfileView(LoginRequiredMixin, FormView):
form_class = ProfileForm
def form_valid(self, form):
if "avatar" in form.changed_data and form.instance.avatar is not None:
form.instance.avatar = process_avatar_image(form.instance.avatar)
form.save()
messages.success(self.request, _("profile updated successfully!"))
return redirect("profile", username=form.instance.username)