123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- =====================
- The "sites" framework
- =====================
- .. module:: django.contrib.sites
- :synopsis: Lets you operate multiple websites from the same database and
- Django project
- Django comes with an optional "sites" framework. It's a hook for associating
- objects and functionality to particular websites, and it's a holding place for
- the domain names and "verbose" names of your Django-powered sites.
- Use it if your single Django installation powers more than one site and you
- need to differentiate between those sites in some way.
- The sites framework is mainly based on this model:
- .. class:: models.Site
- A model for storing the ``domain`` and ``name`` attributes of a website.
- .. attribute:: domain
- The fully qualified domain name associated with the website.
- For example, ``www.example.com``.
- .. attribute:: name
- A human-readable "verbose" name for the website.
- The :setting:`SITE_ID` setting specifies the database ID of the
- :class:`~django.contrib.sites.models.Site` object associated with that
- particular settings file. If the setting is omitted, the
- :func:`~django.contrib.sites.shortcuts.get_current_site` function will
- try to get the current site by comparing the
- :attr:`~django.contrib.sites.models.Site.domain` with the host name from
- the :meth:`request.get_host() <django.http.HttpRequest.get_host>` method.
- How you use this is up to you, but Django uses it in a couple of ways
- automatically via a couple of conventions.
- Example usage
- =============
- Why would you use sites? It's best explained through examples.
- Associating content with multiple sites
- ---------------------------------------
- The LJWorld.com_ and Lawrence.com sites were operated by the same news
- organization -- the Lawrence Journal-World newspaper in Lawrence, Kansas.
- LJWorld.com focused on news, while Lawrence.com focused on local entertainment.
- But sometimes editors wanted to publish an article on *both* sites.
- The naive way of solving the problem would be to require site producers to
- publish the same story twice: once for LJWorld.com and again for Lawrence.com.
- But that's inefficient for site producers, and it's redundant to store
- multiple copies of the same story in the database.
- A better solution removes the content duplication: Both sites use the same
- article database, and an article is associated with one or more sites. In
- Django model terminology, that's represented by a
- :class:`~django.db.models.ManyToManyField` in the ``Article`` model::
- from django.contrib.sites.models import Site
- from django.db import models
- class Article(models.Model):
- headline = models.CharField(max_length=200)
- # ...
- sites = models.ManyToManyField(Site)
- This accomplishes several things quite nicely:
- * It lets the site producers edit all content -- on both sites -- in a
- single interface (the Django admin).
- * It means the same story doesn't have to be published twice in the
- database; it only has a single record in the database.
- * It lets the site developers use the same Django view code for both sites.
- The view code that displays a given story checks to make sure the requested
- story is on the current site. It looks something like this::
- from django.contrib.sites.shortcuts import get_current_site
- def article_detail(request, article_id):
- try:
- a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id)
- except Article.DoesNotExist:
- raise Http404("Article does not exist on this site")
- # ...
- .. _ljworld.com: https://www2.ljworld.com/
- Associating content with a single site
- --------------------------------------
- Similarly, you can associate a model to the
- :class:`~django.contrib.sites.models.Site`
- model in a many-to-one relationship, using
- :class:`~django.db.models.ForeignKey`.
- For example, if an article is only allowed on a single site, you'd use a model
- like this::
- from django.contrib.sites.models import Site
- from django.db import models
- class Article(models.Model):
- headline = models.CharField(max_length=200)
- # ...
- site = models.ForeignKey(Site, on_delete=models.CASCADE)
- This has the same benefits as described in the last section.
- .. _hooking-into-current-site-from-views:
- Hooking into the current site from views
- ----------------------------------------
- You can use the sites framework in your Django views to do
- particular things based on the site in which the view is being called.
- For example::
- from django.conf import settings
- def my_view(request):
- if settings.SITE_ID == 3:
- # Do something.
- pass
- else:
- # Do something else.
- pass
- It's fragile to hard-code the site IDs like that, in case they change. The
- cleaner way of accomplishing the same thing is to check the current site's
- domain::
- from django.contrib.sites.shortcuts import get_current_site
- def my_view(request):
- current_site = get_current_site(request)
- if current_site.domain == "foo.com":
- # Do something
- pass
- else:
- # Do something else.
- pass
- This has also the advantage of checking if the sites framework is installed,
- and return a :class:`~django.contrib.sites.requests.RequestSite` instance if
- it is not.
- If you don't have access to the request object, you can use the
- ``get_current()`` method of the :class:`~django.contrib.sites.models.Site`
- model's manager. You should then ensure that your settings file does contain
- the :setting:`SITE_ID` setting. This example is equivalent to the previous one::
- from django.contrib.sites.models import Site
- def my_function_without_request():
- current_site = Site.objects.get_current()
- if current_site.domain == "foo.com":
- # Do something
- pass
- else:
- # Do something else.
- pass
- Getting the current domain for display
- --------------------------------------
- LJWorld.com and Lawrence.com both have email alert functionality, which lets
- readers sign up to get notifications when news happens. It's pretty basic: A
- reader signs up on a web form and immediately gets an email saying,
- "Thanks for your subscription."
- It'd be inefficient and redundant to implement this sign up processing code
- twice, so the sites use the same code behind the scenes. But the "thank you for
- signing up" notice needs to be different for each site. By using
- :class:`~django.contrib.sites.models.Site`
- objects, we can abstract the "thank you" notice to use the values of the
- current site's :attr:`~django.contrib.sites.models.Site.name` and
- :attr:`~django.contrib.sites.models.Site.domain`.
- Here's an example of what the form-handling view looks like::
- from django.contrib.sites.shortcuts import get_current_site
- from django.core.mail import send_mail
- def register_for_newsletter(request):
- # Check form values, etc., and subscribe the user.
- # ...
- current_site = get_current_site(request)
- send_mail(
- "Thanks for subscribing to %s alerts" % current_site.name,
- "Thanks for your subscription. We appreciate it.\n\n-The %s team."
- % (current_site.name,),
- "editor@%s" % current_site.domain,
- [user.email],
- )
- # ...
- On Lawrence.com, this email has the subject line "Thanks for subscribing to
- lawrence.com alerts." On LJWorld.com, the email has the subject "Thanks for
- subscribing to LJWorld.com alerts." Same goes for the email's message body.
- Note that an even more flexible (but more heavyweight) way of doing this would
- be to use Django's template system. Assuming Lawrence.com and LJWorld.com have
- different template directories (:setting:`DIRS <TEMPLATES-DIRS>`), you could
- farm out to the template system like so::
- from django.core.mail import send_mail
- from django.template import loader
- def register_for_newsletter(request):
- # Check form values, etc., and subscribe the user.
- # ...
- subject = loader.get_template("alerts/subject.txt").render({})
- message = loader.get_template("alerts/message.txt").render({})
- send_mail(subject, message, "editor@ljworld.com", [user.email])
- # ...
- In this case, you'd have to create :file:`subject.txt` and :file:`message.txt`
- template files for both the LJWorld.com and Lawrence.com template directories.
- That gives you more flexibility, but it's also more complex.
- It's a good idea to exploit the :class:`~django.contrib.sites.models.Site`
- objects as much as possible, to remove unneeded complexity and redundancy.
- Getting the current domain for full URLs
- ----------------------------------------
- Django's ``get_absolute_url()`` convention is nice for getting your objects'
- URL without the domain name, but in some cases you might want to display the
- full URL -- with ``https://`` and the domain and everything -- for an object.
- To do this, you can use the sites framework. An example:
- .. code-block:: pycon
- >>> 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/'
- .. _enabling-the-sites-framework:
- Enabling the sites framework
- ============================
- To enable the sites framework, follow these steps:
- #. Add ``'django.contrib.sites'`` to your :setting:`INSTALLED_APPS` setting.
- #. Define a :setting:`SITE_ID` setting::
- SITE_ID = 1
- #. Run :djadmin:`migrate`.
- ``django.contrib.sites`` registers a
- :data:`~django.db.models.signals.post_migrate` signal handler which creates a
- default site named ``example.com`` with the domain ``example.com``. This site
- will also be created after Django creates the test database. To set the
- correct name and domain for your project, you can use a :ref:`data migration
- <data-migrations>`.
- In order to serve different sites in production, you'd create a separate
- settings file with each ``SITE_ID`` (perhaps importing from a common settings
- file to avoid duplicating shared settings) and then specify the appropriate
- :envvar:`DJANGO_SETTINGS_MODULE` for each site.
- Caching the current ``Site`` object
- ===================================
- As the current site is stored in the database, each call to
- ``Site.objects.get_current()`` could result in a database query. But Django is a
- little cleverer than that: on the first request, the current site is cached, and
- any subsequent call returns the cached data instead of hitting the database.
- If for any reason you want to force a database query, you can tell Django to
- clear the cache using ``Site.objects.clear_cache()``::
- # First call; current site fetched from database.
- current_site = Site.objects.get_current()
- # ...
- # Second call; current site fetched from cache.
- current_site = Site.objects.get_current()
- # ...
- # Force a database query for the third call.
- Site.objects.clear_cache()
- current_site = Site.objects.get_current()
- The ``CurrentSiteManager``
- ==========================
- .. class:: managers.CurrentSiteManager
- If :class:`~django.contrib.sites.models.Site` plays a key role in your
- application, consider using the helpful
- :class:`~django.contrib.sites.managers.CurrentSiteManager` in your
- model(s). It's a model :doc:`manager </topics/db/managers>` that
- automatically filters its queries to include only objects associated
- with the current :class:`~django.contrib.sites.models.Site`.
- .. admonition:: Mandatory :setting:`SITE_ID`
- The ``CurrentSiteManager`` is only usable when the :setting:`SITE_ID`
- setting is defined in your settings.
- Use :class:`~django.contrib.sites.managers.CurrentSiteManager` by adding it to
- your model explicitly. For example::
- from django.contrib.sites.models import Site
- from django.contrib.sites.managers import CurrentSiteManager
- from django.db import models
- class Photo(models.Model):
- photo = models.FileField(upload_to="photos")
- photographer_name = models.CharField(max_length=100)
- pub_date = models.DateField()
- site = models.ForeignKey(Site, on_delete=models.CASCADE)
- objects = models.Manager()
- on_site = CurrentSiteManager()
- With this model, ``Photo.objects.all()`` will return all ``Photo`` objects in
- the database, but ``Photo.on_site.all()`` will return only the ``Photo`` objects
- associated with the current site, according to the :setting:`SITE_ID` setting.
- Put another way, these two statements are equivalent::
- Photo.objects.filter(site=settings.SITE_ID)
- Photo.on_site.all()
- How did :class:`~django.contrib.sites.managers.CurrentSiteManager`
- know which field of ``Photo`` was the
- :class:`~django.contrib.sites.models.Site`? By default,
- :class:`~django.contrib.sites.managers.CurrentSiteManager` looks for a
- either a :class:`~django.db.models.ForeignKey` called
- ``site`` or a
- :class:`~django.db.models.ManyToManyField` called
- ``sites`` to filter on. If you use a field named something other than
- ``site`` or ``sites`` to identify which
- :class:`~django.contrib.sites.models.Site` objects your object is
- related to, then you need to explicitly pass the custom field name as
- a parameter to
- :class:`~django.contrib.sites.managers.CurrentSiteManager` on your
- model. The following model, which has a field called ``publish_on``,
- demonstrates this::
- from django.contrib.sites.models import Site
- from django.contrib.sites.managers import CurrentSiteManager
- from django.db import models
- class Photo(models.Model):
- photo = models.FileField(upload_to="photos")
- photographer_name = models.CharField(max_length=100)
- pub_date = models.DateField()
- publish_on = models.ForeignKey(Site, on_delete=models.CASCADE)
- objects = models.Manager()
- on_site = CurrentSiteManager("publish_on")
- If you attempt to use :class:`~django.contrib.sites.managers.CurrentSiteManager`
- and pass a field name that doesn't exist, Django will raise a ``ValueError``.
- Finally, note that you'll probably want to keep a normal
- (non-site-specific) ``Manager`` on your model, even if you use
- :class:`~django.contrib.sites.managers.CurrentSiteManager`. As
- explained in the :doc:`manager documentation </topics/db/managers>`, if
- you define a manager manually, then Django won't create the automatic
- ``objects = models.Manager()`` manager for you. Also note that certain
- parts of Django -- namely, the Django admin site and generic views --
- use whichever manager is defined *first* in the model, so if you want
- your admin site to have access to all objects (not just site-specific
- ones), put ``objects = models.Manager()`` in your model, before you
- define :class:`~django.contrib.sites.managers.CurrentSiteManager`.
- .. _site-middleware:
- Site middleware
- ===============
- If you often use this pattern::
- from django.contrib.sites.models import Site
- def my_view(request):
- site = Site.objects.get_current()
- ...
- To avoid repetitions, add
- :class:`django.contrib.sites.middleware.CurrentSiteMiddleware` to
- :setting:`MIDDLEWARE`. The middleware sets the ``site`` attribute on every
- request object, so you can use ``request.site`` to get the current site.
- How Django uses the sites framework
- ===================================
- Although it's not required that you use the sites framework, it's strongly
- encouraged, because Django takes advantage of it in a few places. Even if your
- Django installation is powering only a single site, you should take the two
- seconds to create the site object with your ``domain`` and ``name``, and point
- to its ID in your :setting:`SITE_ID` setting.
- Here's how Django uses the sites framework:
- * In the :mod:`redirects framework <django.contrib.redirects>`, each
- redirect object is associated with a particular site. When Django searches
- for a redirect, it takes into account the current site.
- * In the :mod:`flatpages framework <django.contrib.flatpages>`, each
- flatpage is associated with a particular site. When a flatpage is created,
- you specify its :class:`~django.contrib.sites.models.Site`, and the
- :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`
- checks the current site in retrieving flatpages to display.
- * In the :mod:`syndication framework <django.contrib.syndication>`, the
- templates for ``title`` and ``description`` automatically have access to a
- variable ``{{ site }}``, which is the
- :class:`~django.contrib.sites.models.Site` object representing the current
- site. Also, the hook for providing item URLs will use the ``domain`` from
- the current :class:`~django.contrib.sites.models.Site` object if you don't
- specify a fully-qualified domain.
- * In the :mod:`authentication framework <django.contrib.auth>`,
- :class:`django.contrib.auth.views.LoginView` passes the current
- :class:`~django.contrib.sites.models.Site` name to the template as
- ``{{ site_name }}``.
- * The shortcut view (``django.contrib.contenttypes.views.shortcut``)
- uses the domain of the current
- :class:`~django.contrib.sites.models.Site` object when calculating
- an object's URL.
- * In the admin framework, the "view on site" link uses the current
- :class:`~django.contrib.sites.models.Site` to work out the domain for the
- site that it will redirect to.
- ``RequestSite`` objects
- =======================
- .. _requestsite-objects:
- Some :doc:`django.contrib </ref/contrib/index>` applications take advantage of
- the sites framework but are architected in a way that doesn't *require* the
- sites framework to be installed in your database. (Some people don't want to,
- or just aren't *able* to install the extra database table that the sites
- framework requires.) For those cases, the framework provides a
- :class:`django.contrib.sites.requests.RequestSite` class, which can be used as
- a fallback when the database-backed sites framework is not available.
- .. class:: requests.RequestSite
- A class that shares the primary interface of
- :class:`~django.contrib.sites.models.Site` (i.e., it has
- ``domain`` and ``name`` attributes) but gets its data from a Django
- :class:`~django.http.HttpRequest` object rather than from a database.
- .. method:: __init__(request)
- Sets the ``name`` and ``domain`` attributes to the value of
- :meth:`~django.http.HttpRequest.get_host`.
- A :class:`~django.contrib.sites.requests.RequestSite` object has a similar
- interface to a normal :class:`~django.contrib.sites.models.Site` object,
- except its :meth:`~django.contrib.sites.requests.RequestSite.__init__()`
- method takes an :class:`~django.http.HttpRequest` object. It's able to deduce
- the ``domain`` and ``name`` by looking at the request's domain. It has
- ``save()`` and ``delete()`` methods to match the interface of
- :class:`~django.contrib.sites.models.Site`, but the methods raise
- :exc:`NotImplementedError`.
- ``get_current_site`` shortcut
- =============================
- Finally, to avoid repetitive fallback code, the framework provides a
- :func:`django.contrib.sites.shortcuts.get_current_site` function.
- .. function:: shortcuts.get_current_site(request)
- A function that checks if ``django.contrib.sites`` is installed and
- returns either the current :class:`~django.contrib.sites.models.Site`
- object or a :class:`~django.contrib.sites.requests.RequestSite` object
- based on the request. It looks up the current site based on
- :meth:`request.get_host() <django.http.HttpRequest.get_host>` if the
- :setting:`SITE_ID` setting is not defined.
- Both a domain and a port may be returned by :meth:`request.get_host()
- <django.http.HttpRequest.get_host>` when the Host header has a port
- explicitly specified, e.g. ``example.com:80``. In such cases, if the
- lookup fails because the host does not match a record in the database,
- the port is stripped and the lookup is retried with the domain part
- only. This does not apply to
- :class:`~django.contrib.sites.requests.RequestSite` which will always
- use the unmodified host.
|