feature: 🖼️ add user avatars
This commit is contained in:
parent
9abf17db4c
commit
bf3775d401
15 changed files with 92 additions and 35 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -75,4 +75,5 @@ dmypy.json
|
|||
.pyre/
|
||||
.pytype/
|
||||
cython_debug/
|
||||
.idea/
|
||||
.idea/
|
||||
media/
|
||||
|
|
@ -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
5
moku/filters.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from emoji import demojize
|
||||
|
||||
|
||||
def unemoji(txt: str):
|
||||
return demojize(txt, delimiters=("", ""))
|
||||
|
|
@ -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
21
moku/images.py
Normal 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)
|
||||
19
moku/migrations/0003_user_avatar.py
Normal file
19
moku/migrations/0003_user_avatar.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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() % {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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=("", ""))
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue