user.save() behaves differently on prod compared to dev environment

I have a CustomUser model and a signup view that overrides Django’s auth. My signup view saves a few fields includingis_active=False. The user is then sent an email with an authorization token, and this token changes is_active to True.

As best as I can tell, this view and corresponding form do what is intended. I don’t get any error while running this on my development environment (inside docker on a local PC). However, when I push my local repository to my production server, it doesn’t behave as expected. The is_active=False is not saved, while is_teacher (and everything else) is saved. As soon as the user submits the signup form, the user is created and is active. I don’t know why my dev and prod environment behave differently, and I don’t know how to fix this.

Model

class CustomUser(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    is_student = models.BooleanField('student status', default=False)
    is_teacher = models.BooleanField('teacher status', default=False)
    SD23 = 'SD23'
    SD39 = 'SD39'
    SD67 = 'SD67'
    SDISTRICT = [
        (SD23, 'Kelowna SD23'),
        (SD39, 'Vancouver SD39'),
        (SD67, 'Summerland SD67'),
    ]
    district = models.CharField(
        max_length=4, choices=SDISTRICT, blank=True, default='SD39')

    paper = models.BooleanField(default=False)

    def __str__(self):
        return self.username

Views

def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False)
            to_email = form.cleaned_data.get('email')
            # make the username the same as the email
            user.username = to_email
            user.is_teacher = True
            user.is_staff = True
            user.is_active = False
            user.save()
            group = Group.objects.get(name='teacher')
            user.groups.add(group)
            current_site = get_current_site(request)

            sendgrid_client = SendGridAPIClient(
                api_key=os.environ.get('SENDGRID_API_KEY'))
            from_email = From("my@email.com")
            to_email = To(to_email)
            subject = "Activate your SmartMark Account"
            active_link = render_to_string('account/acc_active_email_link.html', {
                'user': user,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'token': account_activation_token.make_token(user),
            })

            html_text = f'Hello {user}<br/><p>Please click on the link below</p><a href="{active_link}">{active_link}</a>'
            html_content = HtmlContent(html_text)
            mail = Mail(from_email, to_email, subject,
                        html_content)
            response = sendgrid_client.send(message=mail)

            return redirect(reverse('accounts:account_activation_sent'))

    else:
        form = CustomUserCreationForm()
    return render(request, 'account/signup.html', {'form': form})

def account_activation_sent(request):
    return render(request, 'account/account_activation_sent.html')

Form

class CustomUserCreationForm(UserCreationForm):
    paper = forms.BooleanField(
        label='I agree that keeping a backup paper gradebook is best practice')
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)

    class Meta:
        model = get_user_model()
        fields = ('email', 'first_name', 'last_name', 'district', 'paper')

    def signup(self, request, user):
        user.district = self.cleaned_data['district']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        user.paper = self.cleaned_data['paper']
        user.email = self.cleaned_data['email']

    def check_suffix(self, e, d):
	"""custom function that compares email address to the 'district' field"""
        email_domain = e.split("@", 2)[1]
        t_suffix = email_creation(d)[0]
        print(email_domain)
        print(t_suffix)
        if email_domain == t_suffix:
            return True
        else:
            return False

    def clean_email(self):
        value = self.cleaned_data["email"]
        value_district = self.data["district"]
        if not value:
            raise forms.ValidationError('An Email address is required.')
        check_users = CustomUser.objects.filter(email__iexact=value)
        if check_users:
            raise forms.ValidationError('This email is already in use.')
        if value and not check_users:
            if not self.check_suffix(value, value_district):
                self.add_error(
                    "email", "Your email address does not match your school district.")
        return value

Any ideas on why this is happening?

So you’ve verified, by checking the database that, when your browser is redirected to account_activation_sent, the is_active field on the new user is True?

You could configure your database to log all inserts & updates and look to see the sequence of events from the database perspective.

You could also configure Django to log all database requests (for a brief period of time!)

You could toss some print statements in at various locations to see what the state of the system is.

What are all the known conditions in your system (leaving aside any that might exist in Django core, but including any in any third-party libraries you may be using) that will change is_active to True?

What are all the configuration differences between your development and production environment? Are you actually sending out those messages in development? Through the same services?

You’re sending out a link - what does that link do? (Specifically, and in detail)

Does that link automatically activate the account? Or does it require some type of user interaction at that point.

Most importantly, do you see that link being requested in your server logs?

(My first reaction to this is that one of the services where you’re sending out your messages is for some reason hitting that link. My conjecture would be that that is the significant difference between your development and production environment. But that’s really a WAG at this point.)

1 Like

I second Ken’s opinions, but especially this one:

Additionally, I’m making the assumption that this project is in the early stages since you’re building out authentication. If that’s the case, there’s a possibility that production is running different code than your local machine. Ken’s suggestion of putting some logging or print statements in the view would also serve to eliminate that scenario.

I’m using dokku in my deployment, where I git push my local repository to my prod server. The files should be exactly the same. I’ve had the prod running for a while but didn’t realize the is_active issue because it was working as expected on my local environment. I’ve added some print statements:

def signup(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
        ....
        mail = Mail(from_email, to_email, subject,
                        html_content)
            response = sendgrid_client.send(message=mail)
            print(user.is_active)  # new, prints False
            user_pk = user.pk  # new

            return HttpResponseRedirect(reverse('accounts:account_activation_sent', args=[user_pk]))  #new

   else:
        form = CustomUserCreationForm()
    return render(request, 'account/signup.html', {'form': form})


def account_activation_sent(request, pk):
    u = CustomUser.objects.get(pk=pk)  # new, prints False
    print(u.is_active)  #new
    return render(request, 'account/account_activation_sent.html')

What I know from this is the following. Just before the signup view redirects, user.is_active prints False. I pass the user pk to the account_activation_sent view and once again I see that user.is_active prints False. At this point I then tried logging into my site with the new user and I was able to. I then logged into django shell as admin, queried the user and saw that user.is_active prints True. So something is happening after being redirected to account_activation_sent but before the user goes to their email to get the authorization token.

The user is sent an email with an authorization token. This view is shown below. I haven’t checked if this view is somehow being hit somehow without the user respoding to the authorization email. I was assuming it was not because for my tests I’m not even opening the authorization email let alone clicking on the url it contains. I will try to check this tonight.

def activate(request, uidb64, token):
    try:
        uid = force_text(urlsafe_base64_decode(uidb64))
        user = CustomUser.objects.get(pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None

    if user is not None and account_activation_token.check_token(user, token):
        user.is_active = True
        user.save()
        login(request, user)
        return redirect('home')
    else:
        return render(request, 'account/account_activation_invalid.html')


def login_view(request):
    if request.method == 'POST':
        form = CustomAuthenticationForm(request.POST)
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                login(request, user)
                return redirect(reverse('home'))
        else:
            messages.error(request, 'username or password not correct')
            return redirect(reverse('login'))

    else:
        form = CustomAuthenticationForm()
    return render(request, 'account/login.html', {'form': form})

I’m not aware of anything in third parties that might change is_active. I’m using whitenoise, sendgrid, django-filter, etc but nothing that should touch the user model AFAIK. Config differences are the same between dev and prod. Things that are in my dev .env file are copied to the dokku container. These configs are secret_key are sendgrid api info. I searched through my repository for the phrase is_active and only found it in the expected views, along with a decorator that checks permissions.

I haven’t yet learned how to log inserts and updates on my databse, or configure Django to log database requests.

That’s your next check. I know that some spam filters / antivirus systems will verify URLs embedded in emails, causing URLs to be hit before the emails are delivered. (They’ll check links to ensure that the domain being displayed matches the domain in the link, and that the url is valid and not matching one in a reject list.)

1 Like

Yes! My activation view def activate is being triggered immediately.

FWIW this is a gradebook app for teachers. The teachers are signing up using their work email address. I believe I can ask my school district’s IT department to whitelist my domain (I think they will?). That would work. However, I guess a better solution would be for the activation view to not automatically authenticate, but wait for some input from the user, correct? Like a checkbox asking the user to agree they requested the authentication. Does that sound right?

Yes, that’s basically the way to do it. Make the activation occur on a post, not the get.

It can even be something a simple as just having a button on the page to press that will take them to the login page. Activate the user in the POST handler before redirecting them to the login page.

1 Like