add app user management allauth
This commit is contained in:
0
aseusers/__init__.py
Normal file
0
aseusers/__init__.py
Normal file
4
aseusers/admin.py
Normal file
4
aseusers/admin.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from .models import Profile
|
||||
|
||||
admin.site.register(Profile)
|
||||
9
aseusers/apps.py
Normal file
9
aseusers/apps.py
Normal 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
22
aseusers/forms.py
Normal 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']
|
||||
27
aseusers/migrations/0001_initial.py
Normal file
27
aseusers/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
aseusers/migrations/__init__.py
Normal file
0
aseusers/migrations/__init__.py
Normal file
24
aseusers/models.py
Normal file
24
aseusers/models.py
Normal 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
37
aseusers/signals.py
Normal 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()
|
||||
21
aseusers/templates/aseusers/profile.html
Normal file
21
aseusers/templates/aseusers/profile.html
Normal 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 %}
|
||||
14
aseusers/templates/aseusers/profile_delete.html
Normal file
14
aseusers/templates/aseusers/profile_delete.html
Normal 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 %}
|
||||
55
aseusers/templates/aseusers/profile_edit.html
Normal file
55
aseusers/templates/aseusers/profile_edit.html
Normal 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 %}
|
||||
58
aseusers/templates/aseusers/profile_settings.html
Normal file
58
aseusers/templates/aseusers/profile_settings.html
Normal 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
3
aseusers/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
aseusers/urls.py
Normal file
12
aseusers/urls.py
Normal 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
93
aseusers/views.py
Normal 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')
|
||||
Reference in New Issue
Block a user