jazzband / django-invitations

Generic invitations app for Django
GNU General Public License v3.0
559 stars 166 forks source link

Feature Request: User Instance Creation on Invitation Acceptance #234

Closed abe-101 closed 2 months ago

abe-101 commented 10 months ago

Hello,

I'm exploring the django-invitations library for a project and I have a specific requirement. I want to be able to invite a user such that when they accept the invitation, a new User instance is automatically created with a foreign key to another model. Additionally, I would like to specify the instance of the associated model at the time the invitation is created.

Is this functionality supported by the library? If not, could you provide guidance on how this might be implemented?

Thank you for your assistance.

krystofbe commented 9 months ago

I am doing something similar with a custom InvitationModel and allauth's user_signed_up signal. here are some snippets from my code.


class CustomInvitation(TimestampedUUIDModel, AbstractBaseInvitation):
    email = models.EmailField(
        unique=True,
        verbose_name=_("e-mail address"),
        max_length=app_settings.EMAIL_MAX_LENGTH,
    )
    created = models.DateTimeField(verbose_name=_("created"), default=timezone.now)
    first_name = models.CharField(max_length=30, verbose_name=_("first name"))
    last_name = models.CharField(max_length=30, verbose_name=_("last name"))
    phone_number = models.CharField(
        max_length=25,
        verbose_name=_("phone number"),
        blank=True,
    )
    driving_school = models.ForeignKey(DrivingSchool, on_delete=models.CASCADE, verbose_name=_("Driving School"))
    role = models.CharField(max_length=30, choices=User.ROLE_CHOICES, verbose_name=_("Role"))
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
    object_id = models.UUIDField(null=True, blank=True, verbose_name=_("Object ID"))
    related_object = GenericForeignKey("content_type", "object_id")

invitation = CustomInvitation.create(
                self.cleaned_data["contact_email"],
                inviter=self.user,  # You need to provide the user who sends the invitation
                first_name=self.cleaned_data["contact_first_name"],
                last_name=self.cleaned_data["contact_last_name"],
                phone_number=self.cleaned_data["contact_phone_number"],
                driving_school=driving_school,
                role=User.ROLE_DRIVING_INSTRUCTOR,
                related_object=instructor,
                content_type=ContentType.objects.get_for_model(instructor),
                object_id=instructor.pk,
            )

invitation.sent = timezone.now()
invitation.save()
invitation.send_invitation(self.request)
from allauth.account.signals import user_signed_up

@receiver(user_signed_up)
def create_user_instance_on_signup(sender, **kwargs):
    user = kwargs["user"]

    try:
        invitation = CustomInvitation.objects.get(email__iexact=user.email)
    except CustomInvitation.DoesNotExist:
        return

    # get the role string from the invitation and add the user to the group with the same name
    role = invitation.role
    group = Group.objects.get(name=role)
    user.groups.add(group)

    user.first_name = invitation.first_name
    user.last_name = invitation.last_name
    user.phone_number = invitation.phone_number
    user.role = role

    # special case for driving school contacts, they are staff members and are allowed to access the admin
    if role == User.ROLE_DRIVING_SCHOOL_CONTACT:
        user.is_staff = True
    elif role == User.ROLE_LEARNER_DRIVER:
        license = invitation.related_object.license
        # set status to STATUS_REDEEMED
        license.status = license.STATUS_REDEEMED
        license.redeemed_on = timezone.now()
        license.save()

    user.save()

    related_object = invitation.related_object

    related_object.user = user
    related_object.save()
abe-101 commented 7 months ago

Thank you @krystofbe for sharing those snippets They've help me a lot in building my solution

abe-101 commented 7 months ago

I'm am curious though why you choose to subclass AbstractBaseInvitation as apposed to Invitation? along those lines I'm curious how you went about implementing the create send_invitation and key_expired methods, did you just copy from the Invitation model?

krystofbe commented 5 months ago

Hi @abe-101,

There's no particular reason for choosing to subclass AbstractBaseInvitation over Invitation. It was a design choice that could have gone either way. You're absolutely correct that subclassing Invitation would have been just as viable.

Regarding the implementation of the create, send_invitation, and key_expired methods, yes, I initially copied them from the Invitation model. However, I customized them to fit the specific needs of the application I was working on. The idea was to leverage existing functionality while making necessary adjustments to align with the new subclass's requirements.

You've made a good point, and it's definitely something to consider for future design decisions. Subclassing directly from Invitation could simplify the process, especially if the modifications are minimal.

Thanks for your insight!

Flimm commented 2 months ago

It looks like you've found a solution to your problem, and you don't need any changes to be made to django-allauth. I'm closing this issue. Let us know if I misunderstood.

abe-101 commented 2 months ago

@Flimm If you'd liek I can a section to the docs explaining this technique. We can keep this issue open or just open a new one