Skip to content

Member creation 3 of 3 ‐ Creating an account

Théophile MADET edited this page Jun 28, 2024 · 6 revisions

This third step is optional because not every member needs an account. Typically, investing members can't do much on Tapir except look at their information, so we usually don't create an account for them.

The button to create the account is on the member's page (ShareOwnerDetailView) and links to the CreateUserFromShareOwnerView.

This view is does not have a form class but instead has the form information built-in with the fields class variable.

It's code is fairly short but quite a lot is actually happening. The interesting part is inside form_valid(), which is called once the form has been confirmed valid, so that's when we know we can perform the relevant actions.

What happens in the view, the visible part

A simple summary of what we can directly see happening inside the view:

  1. Link the ShareOwner to the TapirUser. We can already blank the info fields because their value has been copied in the tapir_user in get_form_kwargs(). TapirUser is already created because our view inherits from CreateView.
tapir_user = form.instance
share_owner = self.get_shareowner()
share_owner.user = tapir_user
share_owner.blank_info_fields()
share_owner.save()
  1. Callbacks are called, if any. Callbacks are like events that other apps can register to. They are an attempt to remove dependencies on the shift app inside other apps. You can see where the shift app registers a listener in tapir.shifts.apps.ShiftConfig
for callback in on_welcome_session_attendance_update:
   callback(share_owner)
  1. Any log that was previously linked to the ShareOwner gets linked to the TapirUser instead.
LogEntry.objects.filter(share_owner=share_owner).update(
    user=tapir_user, share_owner=None
)
  1. An email gets sent to the member:
tapir_user.refresh_from_db() 
email = TapirAccountCreatedEmail(tapir_user=tapir_user)
email.send_to_tapir_user(actor=self.request.user, recipient=tapir_user)

What happens during the view, the less visible part

A few more things happen when CreateUserFromShareOwnerView runs that are not directly visible in it's code.

A ShiftUserData objects gets created

In the shifts's app models.py file, a signal listener is created. There are different signals, this one says "Every time a TapirUser is created, call the create_shift_user_data()" function. This way, we can be sure that there always is a ShiftUserData for each TapirUser

models.signals.post_save.connect(create_shift_user_data, sender=TapirUser)

While signals are sometimes useful (in our case they allow the shift app to do somethig without creating a dependency inside the coop app), they are easy to overlook as illustrated here: nothing in the CreateUserFromShareOwnerView shows that a ShiftUserData gets created. So they should only be used when strictly necessary.

Another pitfall with signals is with that they are not always called when you would expect it. Namelly, if we created many TapirUsers with TapirUser.objects.bulk_create(...), our post_save signal would not get called. Similarly, if you update several objects with Model.objects.update(...), signals would not get called.

An LDAP object gets created

For this we have to notice that TapirUser inherits from LdapUser, which overrides the save() function that comes with Django's Model.

The library we use to connect to LDAP (django-ldapdb) allows us to define models such as LdapPerson and LdapGroup that behave almost like normal Django models, except that fetching and saving happens relatively to LDAP's database instead of the usual one Django connects to.

It's a bit hidden in tapir.accounts.models.LdapUser.save but we can see that an LdapPerson object gets created for our user if there is none yet:

ldap_user = LdapPerson(uid=self.username)

This line only creates the object in Django's memory, it is not persited to the database yet. That happens a bit further with ldap_user.save()

At the time of writing the documentation on LDAP is not written yet. In short. LDAP handles user authentificaton outside of Django. This allows us to use the same account for Tapir, the Wiki, Metabase... because they all connect to LDAP

The member must set their own password

We created the account, but it currently doesn't have a password. The member receives an email (TapirAccountCreatedEmail) that behaves like a password-reset email. See these lines.