Skip to content

Commit

Permalink
Edit profile
Browse files Browse the repository at this point in the history
  • Loading branch information
blopker committed Oct 12, 2023
1 parent 51793f4 commit 3b99249
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 25 deletions.
4 changes: 2 additions & 2 deletions assets/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
-webkit-animation: light 1s;
} */

@keyframes light {
/* @keyframes light {
0% {
background-position: -600px;
}
Expand All @@ -89,7 +89,7 @@
100% {
background-position: 0px;
}
}
} */


/* Django */
Expand Down
2 changes: 1 addition & 1 deletion assets/js/components/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function Card(props) {
</a>
)
return (
<div class="relative max-w-[300px] overflow-clip rounded-3xl border border-gray-200 bg-white shadow ">
<div class="relative max-w-[300px] overflow-clip rounded-3xl border border-gray-200 bg-white shadow transition-shadow hover:shadow-xl">
{image}
<div class="p-5">
<div class="flex justify-between">
Expand Down
2 changes: 1 addition & 1 deletion totem/static/css/styles.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion totem/static/js/app.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions totem/static/js/app.min.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions totem/templates/utils/avatar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<img width="{{ size }}"
height="{{ size }}"
src="{{ image.url }}"
alt="{{ name }}"
alt=""
title="{{ name }}"
class="rounded-full bg-tcreme p-0.5 {{ classes }}">
{% else %}
<img width="{{ size }}"
height="{{ size }}"
src="data:image/svg+xml;utf8,{{ svg }}"
alt="{{ name }}"
alt=""
title="{{ name }}"
class="rounded-full bg-tcreme p-0.5 {{ classes }}">
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.5 on 2023-10-12 21:17

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):
dependencies = [
("users", "0020_alter_user_profile_image"),
]

operations = [
migrations.AddField(
model_name="user",
name="profile_avatar_seed",
field=models.UUIDField(default=uuid.uuid4),
),
migrations.AddField(
model_name="user",
name="profile_avatar_type",
field=models.CharField(choices=[("TD", "Tie Dye"), ("IM", "Image")], default="TD", max_length=2),
),
]
16 changes: 15 additions & 1 deletion totem/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db.models import BooleanField, CharField, EmailField, UUIDField
from django.db.models import BooleanField, CharField, EmailField, TextChoices, UUIDField
from django.urls import reverse
from django.utils.html import escape as html_escape
from django.utils.html import strip_tags
Expand Down Expand Up @@ -42,6 +42,10 @@ class User(SluggedModel, AbstractUser):
check forms.SignupForm and forms.SocialSignupForms accordingly.
"""

class ProfileChoices(TextChoices):
TIEDYE = "TD", _("Tie Dye")
IMAGE = "IM", _("Image")

# First and last name do not cover name patterns around the globe
objects: UserManager = UserManager()
name = CharField(_("Name"), blank=True, max_length=255)
Expand All @@ -58,6 +62,12 @@ class User(SluggedModel, AbstractUser):
spec=ProfileImageSpec, # type: ignore
help_text="Profile image, must be under 5mb. Will be cropped to a square.",
)
profile_avatar_seed = UUIDField(default=uuid.uuid4)
profile_avatar_type = CharField(
default=ProfileChoices.TIEDYE,
max_length=2,
choices=ProfileChoices.choices,
)
verified = BooleanField(_("Verified"), default=False)
timezone = TimeZoneField(choices_display="WITH_GMT_OFFSET")

Expand Down Expand Up @@ -107,6 +117,10 @@ def identify(self):
def analytics_id(self):
return settings.ENVIRONMENT_NAME.lower() + "_" + str(self.pk)

def randomize_avatar(self):
self.profile_avatar_seed = uuid.uuid4()
self.save()

def __str__(self):
return f"<User: {self.name}, slug: {self.slug}, email: {self.email}>"

Expand Down
19 changes: 17 additions & 2 deletions totem/users/templates/users/profile_base.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load settings_value %}
{% load avatar %}
{% load static %}
{# djlint:off #}
{% block title %}Profile{% endblock title %}
Expand All @@ -8,8 +9,22 @@
<div class="px-5 profile-section">
<div class="max-w-5xl md:mt-10 mt-5 m-auto md:flex">
<div>
<ul class="menu bg-white md:mr-10 w-56 rounded-box">
<li class="menu-title">Menu</li>
<ul class="menu md:mr-10 w-56">
<div class="mb-5 relative">
<a href="{% url 'users:profile-image' %}">
<div class="relative hover:shadow-md transition-opacity rounded-full p-3 inline-block">
{% avatar user=request.user %}
<div class="absolute bottom-5 rounded-full bg-tcreme p-1 right-5">
<svg width="20"
height="20"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="#000000" d="m19.3 8.925l-4.25-4.2l1.4-1.4q.575-.575 1.413-.575t1.412.575l1.4 1.4q.575.575.6 1.388t-.55 1.387L19.3 8.925ZM17.85 10.4L7.25 21H3v-4.25l10.6-10.6l4.25 4.25Z" />
</svg>
</div>
</div>
</a>
</div>
<li>
<a href="{% url 'users:profile' %}">Profile</a>
</li>
Expand Down
85 changes: 85 additions & 0 deletions totem/users/templates/users/profile_image_edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{% extends "users/profile_base.html" %}
{% load static %}
{% load avatar %}
{% block profile_content %}
<div>
<h2 class="h2 pb-10">Edit Profile Image</h2>
<div class="hx-profile-image-form bg-white rounded-box p-4 inline-flex flex-col justify-center items-center m-auto transition-all duration-300"
hx-select=".hx-profile-image-form"
hx-swap="outerHTML"
hx-target="this">
<script>
function profile_randomize() {
document.getElementsByName("randomize")[0].click();
}
function profile_upload() {
const pimage = document.getElementsByName("profile_image")[0];
pimage.onchange = function() {
pimage.form.submit();
}
pimage.click();
}
function profile_action() {
{% if user.profile_avatar_type == "IM" %}
profile_upload();
{% elif user.profile_avatar_type == "TD" %}
profile_randomize();
{% endif %}
}
</script>
<div class="relative inline-block cursor-pointer mb-5"
onclick="profile_action()">
{% avatar user=request.user blank_ok=True %}
<div class="opacity-0 rounded-full w-full h-full bg-tcreme hover:opacity-100 transition-opacity absolute top-0 left-0 bg-opacity-70">
{% if user.profile_avatar_type == "IM" %}
<h3 class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">Upload</h3>
{% elif user.profile_avatar_type == "TD" %}
<h3 class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">Randomize</h3>
{% endif %}
</div>
<div class="absolute bottom-2 rounded-full bg-white p-1 right-2">
{% if user.profile_avatar_type == "IM" %}
<svg width="25"
class="absolute bottom-0 rounded-full bg-white p-1 right-0"
height="25"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="#000000" d="M11 16V7.85l-2.6 2.6L7 9l5-5l5 5l-1.4 1.45l-2.6-2.6V16h-2Zm-5 4q-.825 0-1.413-.588T4 18v-3h2v3h12v-3h2v3q0 .825-.588 1.413T18 20H6Z" />
</svg>
{% elif user.profile_avatar_type == "TD" %}
<svg width="25"
class="absolute bottom-0 rounded-full bg-tcreme p-1 right-0"
height="25"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<path fill="#000000" d="M341.3 28.3v85.3H128c-70.7 0-128 57.3-128 128c0 21.5 5.8 41.4 15.2 59.2L68 263.2c-2.4-6.8-4-13.9-4-21.5c0-35.4 28.7-64 64-64h213.3V263L512 156.3V135L341.3 28.3zM444 262.8c2.4 6.8 4 13.9 4 21.5c0 35.4-28.6 64-64 64H170.7V263L0 369.7V391l170.7 106.7v-85.3H384c70.7 0 128-57.3 128-128c0-21.5-5.8-41.4-15.2-59.2L444 262.8z" />
</svg>
{% endif %}
</div>
</div>
<div>
<form hx-post="{% url "users:profile-image" %}"
action="{% url "users:profile-image" %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<button type="submit" class="hidden" name="randomize" value="true"></button>
<input type="file"
class="hidden"
name="profile_image"
accept="image/png, image/jpeg">
<div class="tabs tabs-boxed inline-block">
{% for choice in form.fields.profile_avatar_type.choices %}
<button type="submit"
name="profile_avatar_type"
value="{{ choice.0 }}"
class="tab {% if choice.0 == user.profile_avatar_type %}tab-active{% endif %}">
{{ choice.1 }}
</button>
{% endfor %}
</div>
</form>
</div>
</div>
</div>
{% endblock profile_content %}
2 changes: 2 additions & 0 deletions totem/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
user_detail_view,
user_index_view,
user_profile_delete_view,
user_profile_image_view,
user_profile_info_view,
user_profile_notifications_view,
user_profile_view,
Expand All @@ -23,6 +24,7 @@
path("profile/info", user_profile_info_view, name="profile-info"),
path("profile/notifications", user_profile_notifications_view, name="profile-notifications"),
path("profile/delete", user_profile_delete_view, name="profile-delete"),
path("profile/image", user_profile_image_view, name="profile-image"),
path("", user_index_view, name="index"),
path("u/<str:slug>/", view=user_detail_view, name="detail"),
]
31 changes: 31 additions & 0 deletions totem/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,37 @@ def user_profile_notifications_view(request):
)


class ProfileForm(forms.ModelForm):
profile_avatar_type = forms.ChoiceField(
required=False,
choices=User.ProfileChoices.choices,
)
profile_image = forms.ImageField(
required=False,
)
randomize = forms.BooleanField(required=False)

class Meta:
model = User
fields = ("profile_avatar_type", "profile_image")


@login_required
def user_profile_image_view(request):
user = request.user
form = ProfileForm(request.POST, request.FILES, instance=user)
if request.method == "POST":
if form.is_valid():
if form.cleaned_data["randomize"]:
user.randomize_avatar()
form.save()
return render(
request,
"users/profile_image_edit.html",
context={"choices": User.ProfileChoices.choices, "user": request.user, "form": form},
)


@login_required
def user_profile_delete_view(request):
if request.method == "POST":
Expand Down
29 changes: 19 additions & 10 deletions totem/utils/templatetags/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@
from django.template import Context
from django.template.engine import Engine

from totem.users.models import User
from totem.utils.hash import basic_hash

register = template.Library()


@register.inclusion_tag("utils/avatar.html")
def avatar(user, size=120, classes=""):
def avatar(user: User, size=120, blank_ok=False, classes=""):
current_engine = Engine.get_default()
size = int(size)
if user.profile_image:
if user.profile_avatar_type == User.ProfileChoices.IMAGE and user.profile_image:
ctx = {"is_image": True, "size": size, "classes": classes, "name": user.name, "image": user.profile_image}
elif user.profile_avatar_type == User.ProfileChoices.IMAGE and blank_ok:
ctx = ctx = {
"is_image": False,
"size": size,
"classes": classes,
"name": user.name,
"svg": urllib.parse.quote(_upload_svg),
}
else:
# Render the avatar as a data URI SVG in an img tag. Using raw SVG causes React to not render the SVG properly.
avatar_ctx = avatar_marble(name=user.name, salt=user.slug, size=size)
avatar_ctx = avatar_marble(salt=str(user.profile_avatar_seed), size=size)
svg = current_engine.select_template(["utils/avatar.svg"]).render(Context(avatar_ctx))
ctx = {"is_image": False, "size": size, "classes": classes, "name": user.name, "svg": urllib.parse.quote(svg)}
return ctx
Expand All @@ -27,6 +36,10 @@ def avatar(user, size=120, classes=""):
ELEMENTS = 3
SIZE = 80

_upload_svg = """<svg width="128" height="128" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#000000" d="M11 16V7.85l-2.6 2.6L7 9l5-5l5 5l-1.4 1.45l-2.6-2.6V16h-2Zm-5 4q-.825 0-1.413-.588T4 18v-3h2v3h12v-3h2v3q0 .825-.588 1.413T18 20H6Z"/>
</svg>"""

_themes = [
["A7C5C5", "DEE0D5", "E2AC48", "B96028", "983C2D"],
["416067", "A76A59", "72E790", "F4EC8D", "CC84CD"],
Expand Down Expand Up @@ -147,15 +160,11 @@ def get_unit(seed, max_value, decimal_places=0):


def avatar_marble(
name: str,
salt: str = "",
salt: str,
colors: list | None = None,
size: int = 120,
):
# Generate avatar from name + salt, where the salt is unique to the user.
# This creates a unique avatar for each person, since every person is unique.
# However, if a user changes their name, the avatar will change.
hashed_key = int(basic_hash(name + salt, as_int=True))
hashed_key = int(basic_hash(salt, as_int=True))
if not colors:
colors = _themes[hashed_key % len(_themes)]
properties = _generate_colors(hashed_key, colors)
Expand All @@ -175,7 +184,7 @@ def avatar_marble(
if __name__ == "__main__":
print(
avatar_marble(
name="868ce1c742d4451efea8",
salt="test",
size=80,
)
)
5 changes: 2 additions & 3 deletions totem/utils/tests/test_avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@
{"color": "#9A8570", "rotate": 221.8, "scale": 1.4, "translateX": 4.9, "translateY": 4.93},
{"color": "#9A8570", "rotate": 30.6, "scale": 1.2, "translateX": 0.7, "translateY": 0.68},
],
"salt": "868ce1c742d4451efea8",
"salt": "868ce1c742d4451efea8868ce1c742d4451efea8",
"size": 80,
}


def test_avatar():
assert (
avatar_marble(
name="868ce1c742d4451efea8",
salt="868ce1c742d4451efea8",
salt="868ce1c742d4451efea8868ce1c742d4451efea8",
size=80,
)
== gold
Expand Down

0 comments on commit 3b99249

Please sign in to comment.