[Django]-How can I get the URL (with protocol and domain) in Django (without request)?

26đź‘Ť

âś…

There’s special standard module for this task – Sites Framework.
It adds Site model, which describes specific site. This model has field domain for domain of the project, and a name – human-readable name of the site.

You associate your models with sites. Like so:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site, on_delete=models.CASCADE)

When you need to make an url for the object you may use something like the following code:

   >>> from django.contrib.sites.models import Site
   >>> obj = MyModel.objects.get(id=3)
   >>> obj.get_absolute_url()
   '/mymodel/objects/3/'
   >>> Site.objects.get_current().domain
   'example.com'
   >>> 'https://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
   'https://example.com/mymodel/objects/3/'

This way you can have multiple domains and content spread between them.
Even if you have only one domain I recommend use it to achieve a good standard comfortable way to keep domain settings.

Installatioin is quite easy:

  1. Add 'django.contrib.sites' to your INSTALLED_APPS setting.

  2. Define a SITE_ID setting:

     SITE_ID = 1
    
  3. Run migrate.

35đź‘Ť

TL;DR: There’s no any “standard” “Django-ish” way of doing that, but the DRY principle promoted by the framework assumes the single configuration store, so a custom setting seems to be a good way to go.

By default Django can serve any number of domains from a single instance, and the HTTP request (more accurately, its HTTP_HOST header) is the only thing Django uses to determine the current host.
As your cron jobs are obviously out of the HTTP cycle, you should store your domain somewhere in settings…

# settings.py
DEFAULT_DOMAIN = 'https://foobar.com'
# or, depending on your configuration:
DEFAULT_DOMAIN = 'https://{}'.format(ALLOWED_HOSTS[0])

…with a tiny context processor to make it easier to handle templating:

# yourapp/context_processors.py
from django.conf import settings

def default_domain(request):
    return {'default_domain': settings.DEFAULT_DOMAIN}

…and then use it in your emails:

# yourapp/templates/email/body.html
<a href="{{ default_domain }}{% url 'target' %}">Click here</a>

Alternatively you can make use of the sites framework, but if you’re serving a single domain, the settings-based solution seems much more simpler and cleaner to me.

1đź‘Ť

For my particular scenario, I need to send out an email to an individual triggered by a django signal (When a user raises an issue and assigns it to another user – an email is raised).

As signals cannot access request, it posed an issue as I needed to access {{ request.get_host }} in my template.

The answers to this question didn’t really appeal to me as I’d like it to work dynamically with a change to host (Production Server Domain, Test Server Domain, Local Dev Server). I also didn’t want to create a new field and make the table dependent on it, for the sake of extracting the host from it.

My Solution

The hostname for my project was the name of the vm hosting the server. Therefore I could differentiate them and assign an individual HOST_ADDR.

settings.py

import socket

try:
    HOSTNAME = socket.gethostname()
except:pass

if HOSTNAME == 'test':
    HOST_ADDR = 'http://test-<test-domain>'

elif HOSTNAME == 'production':
    HOST_ADDR = 'http://<production-domain>'
else:
    HOST_ADDR = 'http://localhost:8000'

I could then import this into the signal, and apply as content to my render_to_string function.

models.py

from django.conf import settings

def email_assignee(sender, **kwargs):
    instance = kwargs["instance"]
    if kwargs["created"]:
        email_context = render_to_string('<app_name>/email.html', context={'issue':instance,'host':settings.HOST_ADDR})
        try:
            send_mail(
                'Subject','', None,
                [instance.assignee.email],
                fail_silently=False,
                html_message=email_context
            )
        except Exception as Error:
            print("Email could not be sent! For reason: {}".format(Error))

post_save.connect(email_assignee, sender=Issue)

Then in my template I can use it as an email link, like so:

email.html

{% load static %}

<p>You have a new issue assigned to you from <b>{{ issue.assigned_by }}</b></p>
<a href="{{ host }}{% url 'emissions_dashboard:view_issue' issue.id %}" target="_blank">View it here</a>

0đź‘Ť

If I were you I would use something like celery for cron job & will use the django provided object for now.

0đź‘Ť

I dove into this rabbit hole a little bit and came across this Django ticket on context processors without RequestContext.

The comment on that ticket brought up a good point.
You can define a simple custom template tag to solve the problem.
My problem was that the request context was unaccessible for the cron task (run by celery) but the domain is needed for sending links back to the site dynamically in the email. I did not want to hardcode the site in the templates and I did not want to repeat myself over and over adding the domain to context each time I set up a new emailing task.

The example below worked for my case and was simple enough to put together.

# settings.py

if env_type == "DEV":
    EMAIL_PAGE_DOMAIN = 'localhost:8000'

else:
    EMAIL_PAGE_DOMAIN = f'https://{ALLOWED_HOSTS[0]}'

# <app_name>/templatetags/extras.py

from django import template
from django.conf import settings

register = template.Library()


@register.simple_tag
def email_page_domain():
    return settings.EMAIL_PAGE_DOMAIN



{% comment %} templates/<email_template> {% endcomment %}
{% load extras %}

<a href="{% email_page_domain %}">Call to Action</a>


# <app_name>/tasks.py

# from celery import shared_task
from django.core.mail import send_mail
from django.template.loader import render_to_string

# Could @shared_task for celery worker to hook in if configured
def send_mail():
    context = {
        "name": "sample_user_firstname"
        }
    send_mail(
        subject="elo",
        message=render_to_string("game/mail/<plain_txt_template>.txt", context),
        from_email=None,
        recipient_list=["example@email.com"],
        html_message=render_to_string("game/mail/<html_email_template>.html", context),
    )

Leave a comment