GoodCloud / django-zebra

Forms, widgets, template tags and examples that make Stripe + Django easier.
MIT License
193 stars 68 forks source link

Custom User Model Breaking Profile Functionality #34

Open zetas opened 10 years ago

zetas commented 10 years ago

With Django 1.5 the profile functionality was deprecated in favor of the new custom user model implementation. In my app, I'm using a custom user model for authentication. Currently I'm using Django 1.6.2

When trying to run the example code for stripe:

class UpgradeView(View):
    form_class = StripePaymentForm
    template = 'account/checkout.html'

    def get(self, request, *args, **kwargs):
        return render(request, self.template, {'form': self.form_class()})

    def post(self, request, *args, **kwargs):
        zebra_form = StripePaymentForm(request.POST)
        if zebra_form.is_valid():
            my_profile = request.user.get_profile()
            stripe_customer = stripe.Customer.retrieve(my_profile.stripe_customer_id)
            stripe_customer.card = zebra_form.cleaned_data['stripe_token']
            stripe_customer.save()

            my_profile.last_4_digits = zebra_form.cleaned_data['last_4_digits']
            my_profile.stripe_customer_id = stripe_customer.id
            my_profile.save()
        return render(request, self.template, {'form': zebra_form})

I get an exception saying my custom user model doesn't have the method "get_profile()". Do I need to add some mixins to my user class or just use a profile model anyway?

Here's a copy of my user model and it's manager just in case, note the inclusion of data that would normally be in the profile:

class WUserManager(BaseUserManager):
    def create_user(self, email, password=None):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        """
        Creates and saves a superuser with the given email and password.
        """
        user = self.create_user(email,
            password=password,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

class WUser(AbstractBaseUser):
    email = models.EmailField(
        verbose_name='email address',
        max_length=255,
        unique=True,
        db_index=True,
    )

    first_name = models.CharField(max_length=100, null=True, blank=True)
    last_name = models.CharField(max_length=150, null=True, blank=True)
    address = models.CharField(max_length=200, null=True, blank=True)
    city = models.CharField(max_length=100, null=True, blank=True)
    state = models.CharField(max_length=50, null=True, blank=True)
    zip = models.PositiveIntegerField(max_length=5, null=True, blank=True)
    account_type = models.CharField(max_length=20)
    created = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = WUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def get_full_name(self):
        # The user is identified by their email address
        if self.first_name and self.last_name:
            return self.first_name + ' ' + self.last_name

        return self.email

    def get_short_name(self):
        # The user is identified by their email address
        if self.first_name:
            return self.first_name

        return self.email.split('@')[0]

    def get_license_status(self):
        try:
            license = self.license
        except License.DoesNotExist:
            license = None

        if license is not None and isinstance(license, License):
            return True

        return False

    # On Python 3: def __str__(self):
    def __unicode__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):        
        return True

    @property
    def is_staff(self):
        return self.is_admin

    class Meta:
        verbose_name = 'User'
        verbose_name_plural = 'Users'
leetrout commented 10 years ago

I'm 90% sure that method would have to be added since you're using a custom model. I'll double check later today.

zetas commented 10 years ago

Thanks, I really appreciate you getting back to me, i have a deadline to get this billing module completed by this weekend and I'm running into a brick wall. Already spent 4 days with django-stripe-payments just to toss it for lack of docs and move onto this.

leetrout commented 10 years ago

https://docs.djangoproject.com/en/1.5/ref/settings/#std:setting-AUTH_PROFILE_MODULE

Looks like you can just toss it on there- django isn't going to use it internally.

Try something like:

def get_profile(self):
    return self

and see if that works...

zetas commented 10 years ago

Ok, i added that method to the WUser class and after submitting the charge form again I get this error:

AttributeError at /account/upgrade/
'WUser' object has no attribute 'stripe_customer_id'
zetas commented 10 years ago

Should I just add the attributes as they error out or use a mixin? I'm still a new django dev so I'm a little shaky on the best practice here.

Thanks again for your time.

leetrout commented 10 years ago

Try class WUser(AbstractBaseUser, StripeMixin) where StripeMixin is from zebra.mixins import StripeMixin.

You might also try class WUser(AbstractBaseUser, ZebraMixin) which is the "kitchen sink".

leetrout commented 10 years ago

And look through https://github.com/GoodCloud/django-zebra/blob/master/zebra/mixins.py

I don't know why our autodocs didn't pick up all the docstrings but they're there.

zetas commented 10 years ago

Ok, I tried both mixins, nothing changed, still complaining about a lack of stripe_customer_id. The StripeMixin says it provides a stripe attribute but it doesn't provide stripe_customer_id which seems to be needed at the profile level.

I went ahead and added it with the following settings to the WUser class:

class WUser(AbstractBaseUser, StripeMixin):
    #...
    stripe_customer_id = models.PositiveIntegerField(null=True, blank=True)

Now I get this error when trying to post the credit card form:

InvalidRequestError at /account/upgrade/
Could not determine which URL to request: Customer instance has invalid ID: None
leetrout commented 10 years ago

My bad- That should be WUser(AbstractBaseUser, StripeCustomer) using from zebra.models import StripeCustomer

https://github.com/GoodCloud/django-zebra/blob/master/zebra/models.py

zetas commented 10 years ago

I removed the stripe_customer_id i put in the WUser class and changed to subclass StripeCustomer as you suggest. I still get the same error as before, the "Customer instance has invalid ID: None".

I feel like I'm doing something wrong, maybe I didn't set it up right. Also, this is my first attempt to get Zebra working with a brand new billing system, here's the template contents just in case it helps:

{% extends 'base.html' %}
{% load zebra_tags %}

{% block static_content %}
    {{ block.super }}
    {% zebra_head_and_stripe_key %}
{% endblock %}

{% block content %}
    <div class="body_container_info">
        <div class="body">
                    {% zebra_card_form %}
                <div id="checkout_confirm">
                </div>
            </div>
        </div>
    </div>
{% endblock %}
zetas commented 10 years ago

As I've been digging into this, I started looking at the Stripe Python Library and it looks like it may be easier to implement the library by itself rather than using an abstraction layer like zebra. The two I've tried so far have been extremely uncooperative in my setup which tells me either they are pretty fragile libraries or I'm using some weird edge case environment, what with the custom user model.

I'll leave this issue open for a bit to see if anyone has any thoughts as to how to get it working with my environment, if it's even possible; just to provide an answer to others that may be having or have this issue in the future.

I want to personally thank @leetrout for helping me work through this :+1: