Django 1.7 multisite User model

2019-04-26 16:09发布

问题:

I want to serve a Django application that serves multiple web sites by single database but different user sets. Think like a blog application, it will be used by several domains with different themes, but use same database by adding a site field to models.

I use Django's SitesFramework for that job. But the problem is, I couldn't separate user models for different sites. I want to use same user model with a site field and email field that unique per site.

I tried to extend AbstractUser model like that:

from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Member(AbstractUser):
    username = None
    site = models.ForeignKey(Site)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    on_site = CurrentSiteManager()

    class Meta:
        unique_together = ('site', 'email')

But gives that error: 'Member.email' must be unique because it is named as the 'USERNAME_FIELD'.

What is the best practice for that issue?

回答1:

I hope this approach helps to you:

1) Compose username before save:

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager

class Member(AbstractUser):
    site = models.ForeignKey(Site)
    on_site = CurrentSiteManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = []

    class Meta:
        unique_together = ('site', 'email')

from django.db.models.signals import pre_save
from django.dispatch import receiver

@receiver(pre_save, sender=Member)
def compose_username(sender, instance, **kwargs):
    instance.username = "{0}__{1}".format( instance.email, instance.site_id ) 

2) Then overwrite ModelBackend in your custom auth backend:

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model

class MyModelBackend(ModelBackend):

    def authenticate(self, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        site = kwargs.get('site')
        identifier = "{0}__{1}".format( username, site )
        try:
            user = UserModel.objects.get(username=identifier)
            if user.check_password(password):
                return user
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (#20760).
            UserModel().set_password(password)

3) Remember set your custom backend on settings:

AUTH_USER_MODEL='s1.Member'
SITE_ID = 1
AUTHENTICATION_BACKENDS = ( 'MyApp.MyModule.MyModelBackend',)

4) Include site when authenticate:

>>> from s1.models import Member as M
>>> m1 = M()
>>> m1.site_id = 1
>>> m1.email = 'peter@hello.com'
>>> m1.save()
>>> m1.set_password('hi')
>>> m1.save()
>>> 
>>> from django.contrib.auth import authenticate, login
>>> u=authenticate(username='peter@hello.com', password='hi', site=1)
>>> u
<Member: peter@hello.com_at_1>
>>> 


回答2:

Well if you want to keep the email as the USERNAME_FIELD, which by definition in the User-model must be always unique, you won't be able to repeat it for each site.

There are more than one approaches I can think of that would probably work, but I guess I would do the following:

  • First of all, I wouldn't extend the AbstractUser-model and make a OneToOne dependency to the Site. Because a User is actually allowed to belong to more than one site. So here, the best option imo is to create a Member-model with a ForeignKey to User and a Site field and make those unique_together. So there is only one Member per Site, and a User remains unique. Which is what represents the case better in real life too.

  • Now, when registering a new user for a site, just make a check first if the User (email-Address) already exists, and if so, just assign a new Member to that User. If not, create a new User as well.

First Edit to the question, "what if a User want's to register to another site with different username, password or email?"

If according to my comments, it's OK to share a user account for the sites (and of course the user is aware of this) In the register-process, in the case the user already exists for a given email, then he could be informed that, as an account for that address already exists for the site-a, this user account would be assigned to the membership to site-b. Then, an e-mail with a verify link could be sent, and when confirmed, the new member would be created and assigned to the valid user.

Another approach

If I was wrong assuming, it's ok and even desired to share users among sites, then I guess a whole new approach is needed here:

Extend the AbstractUser as you were doing, but instead of using the email as USERNAME_FIELD, use a new field composed from <email>_<site_id> (which would always be unique, as these 2 fields are unique_together)... the field could be called unique_site_id or so. And this field could be populated after submitting the sign-in and login forms.