add app user management allauth

This commit is contained in:
2025-02-01 19:16:33 +01:00
parent c13049a30f
commit e3ae8300f1
55 changed files with 1147 additions and 465 deletions

0
aseusers/__init__.py Normal file
View File

4
aseusers/admin.py Normal file
View File

@@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Profile
admin.site.register(Profile)

9
aseusers/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AseusersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'aseusers'
def ready(self):
import aseusers.signals

22
aseusers/forms.py Normal file
View File

@@ -0,0 +1,22 @@
from django.forms import ModelForm
from django import forms
from django.contrib.auth.models import User
from .models import Profile
class ProfileForm(ModelForm):
class Meta:
model = Profile
fields = ['image', 'displayname', 'info' ]
widgets = {
'image': forms.FileInput(),
'displayname' : forms.TextInput(attrs={'placeholder': 'Add display name'}),
'info' : forms.Textarea(attrs={'rows':3, 'placeholder': 'Add information'})
}
class EmailForm(ModelForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['email']

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.4 on 2025-01-31 20:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='avatars/')),
('displayname', models.CharField(blank=True, max_length=20, null=True)),
('info', models.TextField(blank=True, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

24
aseusers/models.py Normal file
View File

@@ -0,0 +1,24 @@
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = models.ImageField(upload_to='avatars/', null=True, blank=True)
displayname = models.CharField(max_length=20, null=True, blank=True)
info = models.TextField(null=True, blank=True)
def __str__(self):
return str(self.user)
@property
def name(self):
if self.displayname:
return self.displayname
return self.user.username
@property
def avatar(self):
if self.image:
return self.image.url
return f'{settings.STATIC_URL}images/avatar.svg'

37
aseusers/signals.py Normal file
View File

@@ -0,0 +1,37 @@
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
from allauth.account.models import EmailAddress
from django.contrib.auth.models import User
from .models import Profile
@receiver(post_save, sender=User)
def user_postsave(sender, instance, created, **kwargs):
user = instance
# add profile if user is created
if created:
Profile.objects.create(
user = user,
)
else:
# update allauth emailaddress if exists
try:
email_address = EmailAddress.objects.get_primary(user)
if email_address.email != user.email:
email_address.email = user.email
email_address.verified = False
email_address.save()
except:
# if allauth emailaddress doesn't exist create one
EmailAddress.objects.create(
user = user,
email = user.email,
primary = True,
verified = False
)
@receiver(pre_save, sender=User)
def user_presave(sender, instance, **kwargs):
if instance.username:
instance.username = instance.username.lower()

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/blank.html' %} {% block content %}
<div
class="container d-flex flex-column align-items-center pt-5 px-4"
style="max-width: 32rem"
>
<img
class="w-36 h-36 rounded-circle object-fit-cover mb-4"
src="{{ profile.avatar }}"
height="144"
/>
<div class="text-center">
<h1>{{ profile.name }}</h1>
<div class="text-muted mb-2 mt-n3">@{{ profile.user.username }}</div>
{% if profile.info %}
<div class="mt-4 hyphens-auto">{{ profile.info|linebreaksbr }}</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'layouts/box.html' %} {% block content %}
<h1 class="mb-4">Delete Account</h1>
<p class="mb-4">Are you sure you want to delete your account?</p>
<form method="POST" class="d-flex gap-2">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
Yes, I want to delete my account
</button>
<a class="btn btn-secondary" href="{{ request.META.HTTP_REFERER }}">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends 'layouts/box.html' %} {% block content %} {% if onboarding %}
<h1 class="mb-4">Complete your Profile</h1>
{% else %}
<h1 class="mb-4">Edit your Profile</h1>
{% endif %}
<div class="text-center d-flex flex-column align-items-center">
<img
id="avatar"
class="w-36 h-36 rounded-circle object-fit-cover my-4"
src="{{ user.profile.avatar }} "
height="144"
/>
<div class="text-center" style="max-width: 24rem">
<h1 id="displayname">{{ user.profile.displayname|default:"" }}</h1>
<div class="text-muted mb-2 mt-n3">@{{ user.username }}</div>
</div>
</div>
<form method="POST" enctype="multipart/form-data">
{% csrf_token %} {{ form.as_p }}
<button type="submit" class="btn btn-primary">Submit</button>
{% if onboarding %}
<a class="btn btn-secondary ms-2" href="{% url 'home' %}">Skip</a>
{% else %}
<a class="btn btn-secondary ms-2" href="{{ request.META.HTTP_REFERER }}"
>Cancel</a
>
{% endif %}
</form>
<script>
// This updates the avatar
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
const image = document.querySelector("#avatar");
if (file && file.type.includes("image")) {
const url = URL.createObjectURL(file);
image.src = url;
}
});
// This updates the name
const display_nameInput = document.getElementById("id_displayname");
const display_nameOutput = document.getElementById("displayname");
display_nameInput.addEventListener("input", (event) => {
display_nameOutput.innerText = event.target.value;
});
</script>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends 'layouts/box.html' %} {% block content %}
<h1 class="mb-4">Account Settings</h1>
<table class="table">
<tbody>
<tr>
<th scope="row" class="pt-3 pb-1 fw-bold">Email address</th>
<td id="email-address" class="pt-3 pb-1 ps-3">
{% if user.email %}{{ user.email }}{% else %}No Email{% endif %}
</td>
<td class="pt-3 pb-1 ps-3">
<a
id="email-edit"
class="text-decoration-none text-primary cursor-pointer"
hx-get="{% url 'profile-emailchange' %}"
hx-target="#email-address"
hx-swap="innerHTML"
>
Edit
</a>
</td>
</tr>
<tr class="border-bottom">
<th scope="row" class="pb-3 fw-bold"></th>
<td class="pb-3 ps-3">
{% if user.emailaddress_set.first.verified %}
<span class="text-success">Verified</span>
{% else %}
<span class="text-warning">Not verified</span>
{% endif %}
</td>
<td class="pb-3 ps-3">
<a
href="{% url 'profile-emailverify' %}"
class="text-decoration-none text-primary"
>
{% if not user.emailaddress_set.first.verified %}Verify{% endif %}
</a>
</td>
</tr>
<tr class="border-bottom">
<th scope="row" class="py-3 fw-bold">Delete Account</th>
<td class="py-3 ps-3">Once deleted, account is gone. Forever.</td>
<td class="py-3 ps-3">
<a
href="{% url 'profile-delete' %}"
class="text-decoration-none text-danger"
>
Delete
</a>
</td>
</tr>
</tbody>
</table>
{% endblock %}

3
aseusers/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
aseusers/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path
from aseusers.views import *
urlpatterns = [
path('', profile_view, name="profile"),
path('edit/', profile_edit_view, name="profile-edit"),
path('onboarding/', profile_edit_view, name="profile-onboarding"),
path('settings/', profile_settings_view, name="profile-settings"),
path('emailchange/', profile_emailchange, name="profile-emailchange"),
path('emailverify/', profile_emailverify, name="profile-emailverify"),
path('delete/', profile_delete_view, name="profile-delete"),
]

93
aseusers/views.py Normal file
View File

@@ -0,0 +1,93 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from allauth.account.utils import send_email_confirmation
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout
from django.contrib.auth.models import User
from django.contrib.auth.views import redirect_to_login
from django.contrib import messages
from .forms import *
def profile_view(request, username=None):
if username:
profile = get_object_or_404(User, username=username).profile
else:
try:
profile = request.user.profile
except:
return redirect_to_login(request.get_full_path())
return render(request, 'aseusers/profile.html', {'profile':profile})
@login_required
def profile_edit_view(request):
form = ProfileForm(instance=request.user.profile)
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
if form.is_valid():
form.save()
return redirect('profile')
if request.path == reverse('profile-onboarding'):
onboarding = True
else:
onboarding = False
return render(request, 'aseusers/profile_edit.html', { 'form':form, 'onboarding':onboarding })
@login_required
def profile_settings_view(request):
return render(request, 'aseusers/profile_settings.html')
@login_required
def profile_emailchange(request):
if request.htmx:
form = EmailForm(instance=request.user)
return render(request, 'partials/email_form.html', {'form':form})
if request.method == 'POST':
form = EmailForm(request.POST, instance=request.user)
if form.is_valid():
# Check if the email already exists
email = form.cleaned_data['email']
if User.objects.filter(email=email).exclude(id=request.user.id).exists():
messages.warning(request, f'{email} is already in use.')
return redirect('profile-settings')
form.save()
# Then Signal updates emailaddress and set verified to False
# Then send confirmation email
send_email_confirmation(request, request.user)
return redirect('profile-settings')
else:
messages.warning(request, 'Form not valid')
return redirect('profile-settings')
return redirect('home')
@login_required
def profile_emailverify(request):
send_email_confirmation(request, request.user)
return redirect('profile-settings')
@login_required
def profile_delete_view(request):
user = request.user
if request.method == "POST":
logout(request)
user.delete()
messages.success(request, 'Account deleted, what a pity')
return redirect('home')
return render(request, 'aseusers/profile_delete.html')