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:
-
Add
'django.contrib.sites'
to yourINSTALLED_APPS
setting. -
Define a SITE_ID setting:
SITE_ID = 1
-
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.
- [Django]-How to write setup.py to include a Git repository as a dependency
- [Django]-How to create an empty queryset and to add objects manually in django
- [Django]-How to aggregate (min/max etc.) over Django JSONField data?
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>
- [Django]-How do I send empty response in Django without templates
- [Django]-Django + apache & mod_wsgi: having to restart apache after changes
- [Django]-Django ModelForm with extra fields that are not in the model
0
If I were you I would use something like celery for cron job & will use the django provided object for now.
- [Django]-Django dynamic forms – on-the-fly field population?
- [Django]-Django REST Framework: how to substitute null with empty string?
- [Django]-How to change status of JsonResponse in Django
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),
)
- [Django]-Django Cannot set values on a ManyToManyField which specifies an intermediary model. Use Manager instead
- [Django]-Django : get_or_create Raises duplicate entry with together_unique
- [Django]-Numeric for loop in Django templates