diff --git a/tom_common/admin.py b/tom_common/admin.py index e69de29b..e05ebe88 100644 --- a/tom_common/admin.py +++ b/tom_common/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User + +from tom_common.models import Profile + + +# Define an inline admin descriptor for the TomUser model +# which acts a bit like a singleton +class TomUserInline(admin.StackedInline): + model = Profile + can_delete = False + verbose_name_plural = "profiles" + + +# Define a new User admin +class UserAdmin(BaseUserAdmin): + inlines = [TomUserInline] + + +# Re-register UserAdmin +admin.site.unregister(User) +admin.site.register(User, UserAdmin) diff --git a/tom_common/forms.py b/tom_common/forms.py index 56cf01a7..b3719b8d 100644 --- a/tom_common/forms.py +++ b/tom_common/forms.py @@ -1,6 +1,10 @@ from django import forms from django.contrib.auth.forms import UsernameField from django.contrib.auth.models import User, Group +from django.db import transaction +from crispy_forms.helper import FormHelper + +from tom_common.models import Profile # UserCreationForm was changed for django 4.2 to not allow new users to have case-sensitive variations in # existing usernames. This check breaks our username update process because we use the UserCreationForm rather @@ -30,6 +34,22 @@ def save(self, *args, **kwargs): return instance +class ProfileModelForm(forms.ModelForm): + class Meta: + model = Profile + fields = ('affiliation',) + + +UserProfileInlineFormSet = forms.inlineformset_factory( + User, + Profile, + form=ProfileModelForm, + extra=1, + can_delete=False, + can_order=False, + ) + + class CustomUserCreationForm(UserCreationForm): """ Form used for creation of new users and update of existing users. @@ -43,14 +63,56 @@ class Meta: fields = ('username', 'first_name', 'last_name', 'email', 'groups') field_classes = {'username': UsernameField} + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.user_profile_formset = UserProfileInlineFormSet( + data=kwargs.get('data'), instance=self.instance + ) + + self.helper = FormHelper() + self.form_tag = False + def save(self, commit=True): - user = super(forms.ModelForm, self).save(commit=False) - # Because this form is used for both create and update user, and the user can be updated without modifying the - # password, we check if the password field has been populated in order to set a new one. - if self.cleaned_data['password1']: - user.set_password(self.cleaned_data["password1"]) - if commit: - user.save() - self.save_m2m() - - return user + # If any operations fail, we roll back + with transaction.atomic(): + # Saving the MaterialRequisition first + user = super(forms.ModelForm, self).save(commit=False) + + # Because this form is used for both create and update user, and the user can be updated without modifying + # the password, we check if the password field has been populated in order to set a new one. + if self.cleaned_data['password1']: + user.set_password(self.cleaned_data["password1"]) + if commit: + # Saving the inline formsets + user.save() + self.user_profile_formset.instance = user + self.user_profile_formset.save() + self.save_m2m() + + return user + + # Also needs to be overridden in case any clean method are implemented + def clean(self): + self.user_profile_formset.clean() + super().clean() + + return self.cleaned_data + + # is_valid sets the cleaned_data attribute so we need to override that too + def is_valid(self): + is_valid = True + is_valid &= self.user_profile_formset.is_valid() + is_valid &= super().is_valid() + + return is_valid + + # In case you're using the form for updating, you need to do this too + # because nothing will be saved if you only update field in the inner formset + def has_changed(self): + has_changed = False + + has_changed |= self.user_profile_formset.has_changed() + has_changed |= super().has_changed() + + return has_changed diff --git a/tom_common/migrations/0001_initial.py b/tom_common/migrations/0001_initial.py new file mode 100644 index 00000000..992232a1 --- /dev/null +++ b/tom_common/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2024-11-22 22:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('affiliation', models.CharField(blank=True, max_length=100, null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tom_common/migrations/__init__.py b/tom_common/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tom_common/models.py b/tom_common/models.py index b484a7c9..23886812 100644 --- a/tom_common/models.py +++ b/tom_common/models.py @@ -1,6 +1,8 @@ from django.conf import settings from django.db.models.signals import post_save +from django.db import models from django.dispatch import receiver +from django.contrib.auth.models import User from rest_framework.authtoken.models import Token @@ -8,3 +10,12 @@ def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) + + +class Profile(models.Model): + """Profile model for a TOMToolkit User""" + user = models.OneToOneField(User, on_delete=models.CASCADE) + affiliation = models.CharField(max_length=100, null=True, blank=True) + + def __str__(self): + return f'{self.user.username} Profile' diff --git a/tom_common/signals.py b/tom_common/signals.py new file mode 100644 index 00000000..e2e76191 --- /dev/null +++ b/tom_common/signals.py @@ -0,0 +1,22 @@ +from django.db.models.signals import post_save +from django.contrib.auth.models import User +from django.dispatch import receiver +from tom_common.models import Profile + + +@receiver(post_save, sender=User) +def create_profile(sender, instance, created, **kwargs): + """When a new user is created, create a profile for them.""" + if created: + Profile.objects.create(user=instance) + + +@receiver(post_save, sender=User) +def save_profile(sender, instance, **kwargs): + """When a user is saved, save their profile.""" + # Take advantage of the fact that logging in updates a user's last_login field + # to create a profile for users that don't have one. + try: + instance.profile.save() + except User.profile.RelatedObjectDoesNotExist: + Profile.objects.create(user=instance) diff --git a/tom_common/templates/tom_common/create_user.html b/tom_common/templates/tom_common/create_user.html index f4653e5e..fbe1accb 100644 --- a/tom_common/templates/tom_common/create_user.html +++ b/tom_common/templates/tom_common/create_user.html @@ -9,6 +9,7 @@ {% endif %} {% csrf_token %} {% bootstrap_form form %} +{% bootstrap_formset form.user_profile_formset %} {% buttons %}