Django-allauth实现多用户类型注册

14

编辑

请不要浪费时间阅读问题...这是错误的方法!

查看我的答案,了解正确解决方案的逐步指南(附有说明)

TL;DR

我如何使用django-allauth为私人用户和公司用户实现注册?

我正在遵循的方法(是否正确?)

我有以下models

class PrivateUser(models.Model):
    """Models a private user account"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)


class CompanyUser(models.Model):
    """Models the company's contact person user account"""
    user = models.OneToOneField(User, on_delete=models.CASCADE)


class Company(models.Model):
    """Models the company attributes"""
    contact_person = models.OneToOneField(User, related_name='company')
    name = models.CharField(max_length=50, null=False, blank=False)
    vat_no = models.CharField(
        # some config and validators
    )
    # ... other non-relevant fields

现在,在使用django-allauth的一个注册表单的情况下,我需要区分两个用户PrivateUserCompanyUser。如官方django-allauth文档所述:

ACCOUNT_SIGNUP_FORM_CLASS (=None)

一个字符串,指向自定义表单类(例如myapp.forms.SignupForm),该类在注册期间用于要求用户提供额外的输入(例如新闻通讯订阅、出生日期)。这个类应该实现一个def signup(self, request, user)方法,其中user表示新注册的用户。

因此,为了创建一个独特的表单,我创建了一个抽象模型类,其中包含来自PrivateUserCompanyUser的所有字段加上一个(请注意user_type字段):
class AbstractComprehensiveUser(models.Model):
    """
    Little hackish model class needed to handle one single sign up
    form for multiple users
    """

    USER_TYPE_CHOICES = (
        ('private', 'Private'),
        ('company', 'Company'),
    )

    user_type = models.CharField(
        max_length=10,
        blank=False,
        choices=USER_TYPE_CHOICES
    )

    # Common fields for either private and company users
    first_name = models.CharField(max_length=30, blank=False)
    last_name = models.CharField(max_length=30, blank=False)

    # Company specific fields
    company_name = models.CharField(max_length=50, null=True, blank=True)
    company_vat_no = models.CharField(
        # some config and validators
        null=True,
        blank = True
    )
    # other non-relevant fields

    class Meta:
        abstract = True

注意:这个类中所有非公共字段都有属性 null=Trueblank=True

然后我按照以下方式创建了自定义的 SignupForm

class SignupForm(forms.ModelForm):
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)

    class Meta:
        model = AbstractComprehensiveUser
        fields = (
            # Field to differentiate from private and company
            # user sign up
            'user_type',
            # Common fields for either private and company users
            'first_name', 'last_name',
            # Company specifc fields
            'company_name', 'company_vat_no', # etc etc
        )

现在的想法是使用一个包含两个表单的模板:

  • 一个带有隐藏字段user_type='private',只包含first_namelast_name字段的表单
  • 一个带有隐藏字段user_type='company',包含来自Company模型的字段的表单

然后,在SignupForm中,我将接收到user_type字段,我可以设置适当的表单,例如:

class PrivateUserSignupForm(forms.ModelForm):
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)

    class Meta:
        model = PrivateUser
        fields = ('first_name', 'last_name')

问题在于当我在SignupForm.signup()方法中检索数据时,User模型已经写入数据库。
我希望不保存它,而只是:
  • 验证它
  • signup方法中接收数据以填充正确的表单(PrivateUserSignupFormCompanyUserSignupForm)
  • 验证表单
    • 如果没有错误,则保存用户和其他模型
    • 如果有错误,则不保存任何内容并警告用户有关错误的信息

问题是...

  • 这种方法正确吗?有没有其他方法可以在不引起这些麻烦的情况下完成此操作?
  • 如果这种方法是正确的,我该如何处理上述工作流程?
2个回答

19

简短概括

以上混乱内容都是无用信息!

(最终)正确的解决方案

settings.py中删除ACCOUNT_SIGNUP_FORM_CLASS,我们将不再使用它。

假设有以下models

class PrivateUser(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

class CompanyUser(models.Model):
    contact_person = models.OneToOneField(User, on_delete=models.CASCADE)
    company_name = models.CharField(max_length=50, null=False, blank=False)

现在,我们想要让我们的应用使用不同的表格注册PrivateUserCompanyUser

为了实现这一点,我们将扩展django-allauth的SignupFormSignupView

forms.py中:

from myapp.models import CompanyUser

class CompanySignupForm(SignupForm):
    # declare here all the extra fields in CompanyUser model WITHOUT
    # the OneToOneField to User
    # (N.B: do NOT try to declare Meta class with model=CompanyUser,
    # it won't work!)
    company_name = forms.CharField(max_length=50, required=True, strip=True)

    # Override the save method to save the extra fields
    # (otherwise the form will save the User instance only)
    def save(self, request):
        # Save the User instance and get a reference to it
        user = super(CompanySignupForm, self).save(request)
        # Create an instance of your model with the extra fields
        # then save it.
        # (N.B: the are already cleaned, but if you want to do some
        # extra cleaning just override the clean method as usual)
        company_user = CompanyUser(
            contact_person=user,
            company_name=self.cleaned_data.get('company_name')
        )
        company_user.save()

        # Remember to return the User instance (not your custom user,
        # the Django one), otherwise you will get an error when the
        # complete_signup method will try to look at it.
        return company_user.contact_person

现在,我们有一个 CompanyUser 模型和一个 CompanySignupForm 表单。让我们在 views.py 中创建一个 CompanyUserSignupView 视图,使用以下代码:
class CompanyUserSignupView(SignupView):
    # The referenced HTML content can be copied from the signup.html
    # in the django-allauth template folder
    template_name = 'account/signup_company.html'
    # the previously created form class
    form_class = CompanySignupForm

    # the view is created just a few lines below
    # N.B: use the same name or it will blow up
    view_name = 'company_signup'

    # I don't use them, but you could override them
    # (N.B: the following values are the default)
    # success_url = None
    # redirect_field_name = 'next'

# Create the view (we will reference to it in the url patterns)
company_signup = CompanyUserRegistrationView.as_view()

最后一步,是修改 urls.py 文件:
urlpatterns = [
    # ...
    url(
        r'^accounts/signup/company/$',
        views.company_signup,
        name='signup-company'
    ),
]

现在,只需使用您的浏览器转到http://localhost:8000/accounts/signup/company(或根据您的配置使用正确的URL模式)。
您将找到额外的字段,可以注册公司用户。
现在重复所有先前的步骤,创建一个PrivateSignupForm表单、一个PrivateUserSignupView视图,并添加适当的URL模式,以让用户作为私人注册。

最后警告

django-allauth默认的注册URL仍然有效,除非您使用其中一个URL进行覆盖...您应该这样做!


1
有一段时间没有了,但是如果我没记错的话,您可以将适配注册默认URL的正则表达式添加到您的URL模式中,并将其指向自定义视图。如果它在默认视图之前被评估,那么它就会成为默认值。 - mrnfrancesco
1
啊,谢谢!你需要在settings.py中更改ACCOUNT_FORMS吗?文档(https://django-allauth.readthedocs.io/en/latest/forms.html#signup-allauth-account-forms-signupform)似乎要更改设置文件,但由于我们这里有两个表单,我甚至不确定要将注册表单设置更改为什么。 - L C
3
我认为你的意思是写成company_signup = CompanyUserSignupView.as_view()而不是company_signup = CompanyUserRegistrationView.as_view() - cristian
2
这不是用于多个表单的有效解决方案!!! - cristian
对于未来阅读此答案的任何人:我必须在视图中覆盖get_form_class方法才能使其正常工作,因为SignupView定义了该方法,以便返回您在ACCOUNT_FORMS设置中设置的表单类(如果您设置了的话)。 - Samuel Ochoa
显示剩余5条评论

11

我遇到了同样的问题。我需要使用allauth来处理不同类型的用户配置文件。我扩展了allauth SignupView并将其用作 In my case I have a MemberProfile and PartnerProfile:

#profile models

class MemberProfile(models.Model):
  user = models.OneToOneField(
    settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE,
  )


class PartnerProfile(models.Model):
  user = models.OneToOneField(
    settings.AUTH_USER_MODEL,
    on_delete=models.CASCADE,
  )

我希望为每种类型的个人资料单独创建注册页面。幸运的是,allauth SignupView在form_value()方法中将用户存储在其实例上。我将SignupView扩展为ProfileView,其中需要一个profile_class:

#mixin

from allauth.account.views import SignupView
from allauth.account.forms import SignupForm


class ProfileSignupView(SignupView):

  template_name = 'profiles/register.html'
  success_url = ''  # profile specific success url
  form_class = SignupForm
  profile_class = None  # profile class goes here

  def form_valid(self, form):
    response = super(ProfileSignupView, self).form_valid(form)
    profile = self.profile_class(user=self.user)
    profile.save()

    return response

那么我的观点如下:

#views

from .mixins import ProfileSignupView
from .models import PartnerProfile, MemberProfile

class MemberSignupView(ProfileSignupView):

   success_url = '/member/profile'
   profile_class = MemberProfile


class PartnerSignupView(ProfileSignupView):

    success_url = '/partner/profile'
    profile_class = PartnerProfile

我可以问一下为什么你也写了form_valid方法吗(因为mmfrancesco的回答没有写)? - L C
mmfranco正在扩展SignupForm并覆盖save方法。这是有效的。我决定扩展SignupView并覆盖form_valid方法,该方法也调用表单上的save方法。区别在于,在complete_signup方法被调用(已发送电子邮件通知)之后,我创建Profile。 mmfranco在此之前创建配置文件。如果您的视图是原子性的,则相同。在我的情况下,我实际上有三个配置文件:合作伙伴、成员和管理员。我发现为每个创建一个视图更容易,但使用相同的表单。 - Alex Gustafson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接