From my perspective the difficult part is OTP and interfacing with SMS because I haven't done that before.
The relational design requires modeling real world relationships which usually isn't hard.
Trying to use "@institution" for userid is fraught with potential problems. Normally a registration system lets a user keep entering names until getting one that is unique.
I built a similar system where it is companies rather than schools and consultants (and employees) get linked by foreign key to only one company. That means they only see that company's information. However, any consultant can work for multiple companies. We decided in that case they had to have multiple logins and we recommended they use @company for the sole reason of reminding themselves which company they are currently working on. That works because they can use for example mike@thiscompany and mike@thatcompany etc. The software doesn't care what the userid is and nor should it. Keep things as simple as possible.
Users can have roles. django.auth.group is useful for differentiating between teachers and students re permissions and also your software can then easily discover which group(s) a user is in and adjust accordingly.
Users can have FK relationships not only with schools but between themselves. That would be a many-to-many with "self" to allow users in the student role to be connected to one or more users in the teacher role.
And so it goes. The secret of success is to model the real world relationships as closely as you can. Roles can then confer different access to various capabilities the same way it happens in real life.
Cheers
Mike
--
(Unsigned mail from my phone)