Django Context getting lost in email

I am trying to send a forgot password email in Django. Although even after using print debug statements to make sure that the slug is available in my context, it's keeps throwing an error.

My models.py:

class Restaurant(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    description = models.TextField()
    landing_page_tagline = models.TextField()
    landing_page_image = models.ImageField(upload_to='restaurant_images/')
    address = models.CharField(max_length=255, null=True)
    phone = models.CharField(max_length=20, null=True)
    logo_image = models.ImageField(upload_to='restaurant_images/')
    primary_color = models.CharField(max_length=7)  # Hex color code
    favicon = models.FileField(upload_to='restaurant_favicons/', null=True, blank=True)
    about_us_image1 = models.ImageField(upload_to='restaurant_images/')
    about_us_text1 = models.TextField(blank=True, null=True)
    about_us_image2 = models.ImageField(upload_to='restaurant_images/')
    about_us_text2 = models.TextField(blank=True, null=True)
    about_us_image3 = models.ImageField(upload_to='restaurant_images/')
    about_us_text3 = models.TextField(blank=True, null=True)
    contact_us_image = models.ImageField(upload_to='restaurant_images/')
    map_iframe_src = models.TextField(blank=True, null=True)
    footer_text = models.TextField()
    facebook_link = models.URLField(null=True, blank=True)
    instagram_link = models.URLField(null=True, blank=True)
    youtube_link = models.URLField(null=True, blank=True)
    twitter_link = models.URLField(null=True, blank=True)
    slug = models.SlugField(unique=True, max_length=100, null=True)
    stripe_subscription_id = models.CharField(max_length=255, blank=True, null=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.name

class RestaurantUser(AbstractUser):
    restaurant = models.OneToOneField('Restaurant', on_delete=models.CASCADE, null=True, blank=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    is_restaurant_admin = models.BooleanField(default=False)

    groups = models.ManyToManyField(
        Group,
        related_name='restaurantuser_set',
        blank=True,
        help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
        related_query_name='restaurantuser',
    )
    user_permissions = models.ManyToManyField(
        Permission,
        related_name='restaurantuser_set',
        blank=True,
        help_text='Specific permissions for this user.',
        related_query_name='restaurantuser',
    )

    def save(self, *args, **kwargs):
        if self.is_restaurant_admin:
            self.is_staff = True
        super().save(*args, **kwargs)

My views.py:

class CustomPasswordResetView(PasswordResetView):
    template_name = 'restaurants/forgot_password.html'
    email_template_name = 'restaurants/password_reset_email.html'
    form_class = PasswordResetForm

    def get_success_url(self):
        print(f"DEBUG: get_success_url - restaurant_slug: {self.kwargs.get('restaurant_slug')}")
        return reverse_lazy('password_reset_done', kwargs={'restaurant_slug': self.kwargs['restaurant_slug']})

    def form_valid(self, form):
        email = form.cleaned_data['email']
        restaurant_slug = self.kwargs.get('restaurant_slug')
        print(f"DEBUG: form_valid - restaurant_slug: {restaurant_slug}")
        restaurant = get_object_or_404(Restaurant, slug=restaurant_slug)

        # Get the user associated with the provided email
        UserModel = RestaurantUser
        try:
            user = UserModel.objects.get(email=email)
        except UserModel.DoesNotExist:
            form.add_error('email', ValidationError("No user is associated with this email address."))
            return self.form_invalid(form)

        # Prepare the context
        context = {
            'uid': urlsafe_base64_encode(force_bytes(user.pk)),
            'token': default_token_generator.make_token(user),
            'protocol': self.request.scheme,
            'domain': self.request.get_host(),
            'restaurant_slug': restaurant_slug,
            'restaurant': restaurant,
            'primary_color': restaurant.primary_color,
        }
        print(f"DEBUG: form_valid - context: {context}")

        # Render HTML content
        subject = "Password Reset Requested"
        html_content = render_to_string('restaurants/password_reset_email.html', context)
        text_content = strip_tags(html_content)

        print(f"DEBUG: form_valid - Full html_content:\n{html_content}")

        print(f"DEBUG: form_valid - html_content: {html_content[:100]}...")  # Print first 100 chars

        # Send email using EmailMultiAlternatives
        email = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [email])
        email.attach_alternative(html_content, "text/html")
        email.send()

        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['restaurant_slug'] = self.kwargs.get('restaurant_slug')
        print(f"DEBUG: get_context_data - restaurant_slug: {context['restaurant_slug']}")
        return context
    

from django.contrib.auth.views import PasswordResetDoneView

class CustomPasswordResetDoneView(PasswordResetDoneView):
    template_name = 'restaurants/password_reset_done.html'

    def get(self, request, *args, **kwargs):
        print(f"DEBUG: CustomPasswordResetDoneView.get - kwargs: {kwargs}")
        return super().get(request, *args, **kwargs)


class CustomPasswordResetConfirmView(PasswordResetConfirmView):
    template_name = 'restaurants/reset_password.html'

    def dispatch(self, *args, **kwargs):
        print(f"DEBUG: CustomPasswordResetConfirmView.dispatch - args: {args}")
        print(f"DEBUG: CustomPasswordResetConfirmView.dispatch - kwargs: {kwargs}")
        return super().dispatch(*args, **kwargs)

    def get_success_url(self):
        print(f"DEBUG: CustomPasswordResetConfirmView.get_success_url - kwargs: {self.kwargs}")
        return reverse_lazy('password_reset_complete', kwargs={
            'restaurant_slug': self.kwargs['restaurant_slug'],
        })

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        restaurant_slug = self.kwargs.get('restaurant_slug')
        print(f"DEBUG: CustomPasswordResetConfirmView.get_context_data - restaurant_slug: {restaurant_slug}")
        if not restaurant_slug:
            messages.error(self.request, "Invalid restaurant context. Please try again.")
            return context
        context['restaurant_slug'] = restaurant_slug
        context['restaurant'] = get_object_or_404(Restaurant, slug=restaurant_slug)
        return context

My urls.py:

    path('restaurant/<slug:slug>/', views.restaurant_landing_page_view, name='restaurant_landing_page'),
    path('password-reset/<slug:restaurant_slug>/', CustomPasswordResetView.as_view(), name='password_reset'),
    path('password-reset-done/<slug:restaurant_slug>/', views.CustomPasswordResetDoneView.as_view(), name='password_reset_done'),
    path('password-reset-confirm/<slug:restaurant_slug>/<uidb64>/<token>/', CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('password-reset-complete/<slug:restaurant_slug>/', auth_views.PasswordResetCompleteView.as_view(template_name='restaurants/password_reset_complete.html'), name='password_reset_complete'),

My password_reset_email.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Password Reset</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 0;">
    <div style="background-color: {{ primary_color }}; color: #ffffff; padding: 20px; text-align: center;">
        <h1 style="margin: 0;">{{ restaurant.name }}</h1>
    </div>
    <div style="padding: 20px;">
        <p>Hello,</p>
        <p>Debug Info:</p>
        <p>restaurant_slug: {{ restaurant_slug }}</p>
        <p>uid: {{ uid }}</p>
        <p>token: {{ token }}</p>
        <p>We received a request to reset your password for your {{ restaurant.name }} account. If you did not make this request, please ignore this email.</p>
        <p>To reset your password, click the button below:</p>
        <a href="{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' restaurant_slug=restaurant_slug uidb64=uid token=token %}" style="background-color: {{ primary_color }}; color: #ffffff; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
        <p>If the button above does not work, paste this link into your web browser:</p>
        <p>{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' restaurant_slug=restaurant_slug uidb64=uid token=token %}</p>
        <p>Thank you,<br>{{ restaurant.name }} Team</p>
    </div>
</body>
</html>

The debug statements that I get:

DEBUG: get_context_data - restaurant_slug: dominos
[01/Sep/2024 12:39:18] "GET /password-reset/dominos/ HTTP/1.1" 200 21115
DEBUG: form_valid - restaurant_slug: dominos
DEBUG: form_valid - context: {'uid': 'OA', 'token': 'ccodtk-d092b2b859d47dd4b086be245bd251f6', 'protocol': 'http', 'domain': '127.0.0.1:8000', 'restaurant_slug': 'dominos', 'restaurant': <Restaurant: Dominos>, 'primary_color': '#2445bc'}
DEBUG: form_valid - Full html_content:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Password Reset</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 0;">
    <div style="background-color: #2445bc; color: #ffffff; padding: 20px; text-align: center;">
        <h1 style="margin: 0;">Dominos</h1>
    </div>
    <div style="padding: 20px;">
        <p>Hello,</p>
        <p>Debug Info:</p>
        <p>restaurant_slug: dominos</p>
        <p>uid: OA</p>
        <p>token: ccodtk-d092b2b859d47dd4b086be245bd251f6</p>
        <p>We received a request to reset your password for your Dominos account. If you did not make this request, please ignore this email.</p>
        <p>To reset your password, click the button below:</p>
        <a href="http://127.0.0.1:8000/password-reset-confirm/dominos/OA/ccodtk-d092b2b859d47dd4b086be245bd251f6/" style="background-color: #2445bc; color: #ffffff; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
        <p>If the button above does not work, paste this link into your web browser:</p>
        <p>http://127.0.0.1:8000/password-reset-confirm/dominos/OA/ccodtk-d092b2b859d47dd4b086be245bd251f6/</p>
        <p>Thank you,<br>Dominos Team</p>
    </div>
</body>
</html>
DEBUG: form_valid - html_content: <!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=de...

I am also getting an email with the correct slug.

It's just that I am getting this error every time I try to send an email for password reset:

Internal Server Error: /password-reset/dominos/
Traceback (most recent call last):
  File "/opt/anaconda3/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/views/generic/base.py", line 104, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/utils/decorators.py", line 48, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/utils/decorators.py", line 48, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/utils/decorators.py", line 188, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/utils/decorators.py", line 186, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/contrib/auth/views.py", line 229, in dispatch
    return super().dispatch(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/views/generic/base.py", line 143, in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/views/generic/edit.py", line 151, in post
    return self.form_valid(form)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/abrarshahriar/Documents/bite/restaurants/views.py", line 335, in form_valid
    return super().form_valid(form)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/contrib/auth/views.py", line 242, in form_valid
    form.save(**opts)
  File "/opt/anaconda3/lib/python3.12/site-packages/django/contrib/auth/forms.py", line 455, in save
    self.send_mail(
  File "/opt/anaconda3/lib/python3.12/site-packages/django/contrib/auth/forms.py", line 389, in send_mail
    body = loader.render_to_string(email_template_name, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/backends/django.py", line 107, in render
    return self.template.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/base.py", line 171, in render
    return self._render(context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
    return self.nodelist.render(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/base.py", line 1008, in render
    return SafeString("".join([node.render_annotated(context) for node in self]))
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/base.py", line 969, in render_annotated
    return self.render(context)
           ^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/template/defaulttags.py", line 480, in render
    url = reverse(view_name, args=args, kwargs=kwargs, current_app=current_app)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/urls/base.py", line 88, in reverse
    return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/anaconda3/lib/python3.12/site-packages/django/urls/resolvers.py", line 831, in _reverse_with_prefix
    raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'password_reset_confirm' with keyword arguments '{'restaurant_slug': '', 'uidb64': 'OA', 'token': 'ccodtp-e1c4e789e104119efece2f7c409c5bb3'}' not found. 1 pattern(s) tried: ['password\\-reset\\-confirm/(?P<restaurant_slug>[-a-zA-Z0-9_]+)/(?P<uidb64>[^/]+)/(?P<token>[^/]+)/\\Z']
[01/Sep/2024 12:39:25] "POST /password-reset/dominos/ HTTP/1.1" 500 183966

I cannot understand why this error would be raised even after getting the correct slug in my email.

Back to Top